欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

[数据结构] 1.顺序表

最编程 2024-10-19 19:43:21
...

「前言」

????个人主页: 代码探秘者
????C语言专栏:C语言
????C++专栏: C++
????喜欢的诗句:天行健,君子以自强不息.

pic_8da49713.png

线性表

线性表(List):零个或多个数据元素的有限序列。

线性表的数据集合为{a1,a2,…,an},假设每个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
pic_0fdcab7c.png
在较复杂的线性表中,一个数据元素可以由若干个数据项组成。在这种情况下,常把数据元素称为记录,含有大量记录的线性表又称为文件

1、顺序表的基本概念

概念:用一组地址连续的存储单元依次存储线性表的数据元素,这种存储结构的线性表称为顺序表。

特点:逻辑上相邻的数据元素,物理次序也是相邻的。

只要确定好了存储线性表的起始位置,线性表中任一数据元素都可以随机存取,所以线性表的顺序存储结构是一种随机存取的储存结构,因为高级语言中的数组类型也是有随机存取的特性,所以通常我们都使用数组来描述数据结构中的顺序储存结构,用动态分配的一维数组表示线性表。

顺序表和数组的区别

◦ 顺序表的底层结构是数组,对数组的封装,实现了常用的增删改查等接口

1.1 静态顺序表

概念:使用定长数组存储元素
静态顺序表缺陷:空间给少了不够用,给多了造成空间浪费

 静态顺序表(这里拿里面存储的整形举例)
 typedef int SLDataType;
 #define N 100
 typedef struct SeqList
 {
     SLDatatype arr[N];
     int size;//size是有效数据的个数
 }SL;
  
 解释一下为什么要把存储的类型变为SLDataType
 这么做是为了方便以后修改 使用 不同 的类型 
 还有把数组存储的数据容量N 便于修改

1.2 动态顺序表

动态顺序表的结构更加灵活, 可以对结构进行 增删查改,所以我们下面的接口都是通过动态顺序表实现的

2、顺序表存储结构

//头文件
#include<stdio.h>
#include<stdlib.h>
typedef int ElemType;//假设当前元素类型int
//顺序表使用动态数组实现
typedef struct
{
	ElemType* elem;	//动态数组

	int length;		//长度:顺序表当前元素个数

	int ListSize;	//容量:最多存储的个数
}SqList;

3、构造一个空的顺序表L

  • 开辟动态数组,以及初始化长度容量
  • 返回顺序表
//初始化
SqList SqList_Init(int Size)
{
	SqList t;

	t.elem = (ElemType*)malloc(Size * sizeof(ElemType));
	t.length = 0;		//当前长度0
	t.ListSize = Size;  //容量
    
	return t;			//返回顺序表
}
int main()
{
	SqList sl;				//创建顺序表
    
	sl = SqList_Init(10);	//开容量为10的顺序表	
	return 0;
}

4、检查容量,扩容

  • 可以使用malloc或者realloc

  • 用三目运算符(如果开始是0,可以赋值给它,当然我们这里避免了这种情况)

  • 扩容两倍

    //扩容
    void SqList_Capacity(SqList* t)
    {
    	//插入数据之前先看空间够不够
    	if (t->length == t->ListSize)
    	{
    		//申请空间
    		//malloc calloc realloc  int arr[100] --->增容realloc
    		//三目表达式
    		int newCapacity = t->ListSize == 0 ? 4 : 2 * t->ListSize;
    		ElemType* tmp = (ElemType*)realloc(t->elem, newCapacity * sizeof(ElemType));//要申请多大的空间
    		if (tmp == NULL)
    		{
    			perror("realloc fail!");
    			exit(1);//直接退出程序,不再继续执行
    		}
    		//空间申请成功(赋值回来就行)
    		t->elem = tmp;
    		t->ListSize = newCapacity;
    	}
    }
    

5、指定位置前插入元素

  • 顺序表:不为空

  • 检查容量:顺序表是否满了,满了就扩容

  • 位置:判断位置是否合法

  • 移动:根据插入位置需要移动元素个数/次数,将指定位置开始所以元素向后移动位置(从后往前移动)

  • 插入和长度:插入元素,元素个数(length)+1

  • 插入注意:

    • 1.插入位置超过顺序表长度,直接插入指定位置
    • 2.移动再插入
  //指定位置前插入	这里得用指针,因为得修改
  void SqList_Push(SqList* t,int index,ElemType elem)
  {
  	//检查容量:顺序表是否满了,满了就扩容
  	SqList_Capacity(t);
  	//位置:判断位置是否合法
  	if (index >= t->ListSize || index < 0)
  	{
  		printf("位置不合法");
  		return;
  	}
  
  	//移动和插入
  	
  	//如果插入位置超过顺序表长度,直接插入指定位置
  	if (index >= t->length)
  	{
  		t->elem[t->length] = elem;		//这里插入在最后一个元素下一个,不给它乱插
  		t->length++;
  		return;
  	}
  	//移动再插入
  	for (int i = t->length; i >index; i--)
  	{
  		//最开始  length=length-1  最后一个元素移动
  		//结束    index+1=index	   刚好index位置空出来
  		t->elem[i] = t->elem[i - 1];
  	}
  	t->elem[index] = elem;
  	t->length++;
  	return;
  }

6、删除

6.1 删除指定位置元素

  • 顺序表:不为空

  • 下标是否合法

  • 下标i+1至length-1(最后一个元素的下标)移动(从前往后移动)

  • 顺序表长度-1

void SqList_Pop1(SqList* t, int index)
{
	//顺序表:不为空
	if (t == NULL)
	{
		printf("顺序表为空");
		return;
	}
	if (index >= t->ListSize || index < 0)
	{
		printf("位置不合法");
		return;
	}
	//
	//
	for (int i=index; i<t->length-1; i++)
	{
		//最开始  index=index+1			覆盖index位置元素
		//结束    length-2=length-1	    刚好
		t->elem[i] = t->elem[i+1];
	}
	t->length--;	//元素个数减1
}

6.2 删除所有指定元素

  • 顺序表:不为空
  • 遍历顺序表,找到要删的,依次覆盖
//删除所有指定元素
void SqList_Pop2(SqList* t, ElemType elem)
{
	//顺序表:不为空
	if (t == NULL)
	{
		printf("顺序表为空");
		return;
	}
	//开始遍历
	int j = 0;
	while (j != t->length)
	{
		//不是要找的元素,下一个
		if (t->elem[j] != elem)
		{
			j++;
			continue;
		}
		//是,从前往后移动覆盖
		// j  =j+1
		// length-2=length-1
		for (int i = j; i < t->length-1; i++)
		{
			t->elem[i] = t->elem[i + 1];
		}
		t->length--;		//个数减1

		j++;
	}
}

7、查找

7.1 查找指定位置的元素

  • 顺序表:不为空
  • 位置合法
  • 返回下标对应元素
//获取顺序表某一位置上的元素
ElemType  Find_Elem1(SqList t, int index)
{
	//顺序表:不为空
	if (t.length==0)
	{
		printf("顺序表为空");
		return -1;
	}
	//位置合法
	if (index >= t.length || index < 0)
	{
		printf("位置不合法");
		return -1;
	}
	return t.elem[index];
}

7.2 查找指定元素的下标

  • 用index记录,遍历顺序表,如果找到,返回下标。
  • 如果有多个,默认返回第一个。
//查找元素
int  Find_Elem2(SqList t, ElemType elem)
{
	//默认没找到
	int index = -1;
	for (int i = 0; i < t.length; i++)
	{
		if (t.elem[i] == elem)
		{
			//记录元素下标
			index = i;
			break;
		}
	}
	//没有找到
	if (index == -1)
	{
		printf("元素不存在,没有找到");
		return -1;
	}
	return index;
}

8、打印

  • 根据顺序表长度遍历整个顺序表
  • 遍历每个元素输出
  • 用length还是ListSize呢?当然是前者啦,看看前面数据结构定义!
//打印
void SqList_Print(SqList t)
{
	//                         这个是一个宏定义,可以观察行号
	printf("[%s %d]打印顺序表:",__FUNCTION__,__LINE__);
	for (int i = 0; i < t.length; i++)
	{
		printf("%d ", t.elem[i]);
	}
	printf("\n");

9、销毁

//销毁
void SqList_Destory(SqList* t)
{
	if (t == NULL)
	{
		printf("顺序表为空,不能销毁");
		return;
	}
	//动态数组不为空
	if (t->elem)
	{
		free(t->elem);
		t->elem = NULL;
		t->ListSize = t->length = 0;
	}
}

10、测试

int main()
{
	SqList sl;
	sl = SqList_Init(5);			//开容量为5的顺序表	
	for (int i = 0; i < 10; i++)	//插入
	{
		SqList_Push(&sl, i, i);	
	}
	SqList_Print(sl);				//打印
	//查找指定下标4的元素
	int ret1 = Find_Elem1(sl, 4);	
	printf("查找指定下标4的元素:%d\n", ret1);

	//查找元素的下标
	int ret2 = Find_Elem2(sl, 3);	
	printf("查找元素3的下标:%d\n", ret2);

	//删除指定下标元素3
	printf("删除指定下标元素3\n");
	printf("删除前:");
	SqList_Print(sl);
	SqList_Pop1(&sl, 3);	
	printf("删除后:");
	SqList_Print(sl);
	
	//删除指定元素8
	printf("删除指定元素8");
	SqList_Push(&sl, 5, 8);			//再插入一个8
	printf("删除前:");
	SqList_Print(sl);
	SqList_Pop2(&sl, 8);			//删除指定元素8
	printf("删除后:");
	SqList_Print(sl);


	SqList_Destory(&sl);			//销毁
	return 0;
}

测试结果:
在这里插入图片描述

11、 小结

11.1 顺序表时间复杂度

从以上代码可以很明显的看出,线性表的顺序存储结果在读、存数据是的时间复杂度是O(1),插入、删除操作的时间复杂度是O(n)。

11.2 顺序表的优缺点

优点:

  • 无须为表中元素之间的逻辑关系而增加额外的存储空间;
  • 可以快速的存取表中任一位置的元素

缺点:

  • 插入和删除操作需要移动大量元素
  • 当线性表长度较大时,难以确定存储空间的容量;造成存储空间的“碎片”。