【数据结构】顺序表的增删查改(C语言实现)

文章目录

  • 一、线性表
  • 二、顺序表
    • 1.顺序表的定义
    • 2.顺序表的分类
  • 三、动态顺序表的实现
    • 1.结构的定义
    • 2.顺序表的初始化
    • 3.检查顺序表的容量
    • 4.顺序表的销毁
    • 5.打印顺序表中的数据
    • 6.在头部插入数据
    • 7.在尾部插入数据
    • 8.在pos位置插入数据
    • 9.在头部删除数据
    • 10.在尾部删除数据
    • 11.删除pos位置的数据
    • 12.查找数据
    • 13.修改pos 位置的数据
  • 四、完整代码
    • 1.SeqList.h
    • 2.SeqList.c
    • 3.test.c
  • 五、顺序表的缺陷

一、线性表

线性表的定义:

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使

用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…

线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,

线性表在物理上存储时,通常以数组链式结构的形式存储。

二、顺序表

1.顺序表的定义

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存

储。在数组上完成数据的增删查改。
【数据结构】顺序表的增删查改(C语言实现)_第1张图片

2.顺序表的分类

静态顺序表:使用定长数组存储元素

#define N 100 //数组的最大长度
typedef int SLDataType; //重命名数据类型
struct SeqList
{
	SLDataType arr[N];    //使用定长数组来存储数据
	int size;            //存储数据的个数
}SL;

动态顺序表:使用动态开辟的数组存储。

typedef int SLDataType;//将数据类型重命名为SLDataType
typedef struct SeqList
{
	SLDataType* arr;      //存储空间的地址
	int size;             //存储数据的个数
	int capacity;         //存储空间的大小
}SL;                      //重定义为SL

三、动态顺序表的实现

1.结构的定义

typedef int SLDataType;//将数据类型重命名为SLDataType
typedef struct SeqList
{
	SLDataType* arr;      //存储空间的地址
	int size;             //存储数据的个数
	int capacity;         //存储空间的大小
}SL;                      //重定义为SL

我们将数据类型重命名为SLDataType,这样以后我们管理其他数据类型的时候只需要改一个地方即可。

相对于静态顺序表,动态顺序表多了一个参数capacity,用来记录顺序表当前的容量,当当前的数据个数size和顺序的容量capacity相等时,需要进行扩容。

2.顺序表的初始化

void SeqListInit(SL* psl)
{
	assert(psl);//断言:防止psl为空


	psl->arr = NULL;
	psl->size = psl->capacity = 0;
}

在初始化顺序表时,将数据的空间地址置为空,size和capacity置为0,在需要存放数据时再动态开辟空间即可,也可以在初始化时先动态开辟一块空间。

3.检查顺序表的容量

void CheckCapacity(SL* psl)
{
	//检查容量
	if (psl->size == psl->capacity)
	{
		//将realloc的返回值由一个临时变量保存,防止扩容失败后丢失原来的空间地址
		int newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
		SLDataType* tmp = (int*)realloc(psl->arr, newCapacity * sizeof(SLDataType));
		if (tmp == NULL)//判空
		{
			perror("realloc fail");//打印错误信息
			return;
			//exit(-1);//退出程序
		}

		psl->arr = tmp;//开辟的空间地址赋给psl->arr
		psl->capacity = newCapacity;//新的空间大小
	}
}

当size和capacity相等时,表示数组已经放满了数据,这时就需要进行扩容,我们一般扩容到原来的2倍,

因为扩容多了容易造成空间浪费,扩容少了,会频繁扩容,效率损失,2倍比较合适,1.5倍也可以。我们在动态开辟时,用一个临时变量接收realloc的返回时,这样就可以防止在扩容失败的时候丢失原来的空间。

4.顺序表的销毁

void SeqListDestroy(SL* psl)
{
	assert(psl);//断言:防止psl为空

	/*if (psl->arr)
	{*/
		free(psl->arr); //释放(避免内存泄漏)
		psl->arr = NULL;//指针置空(避免野指针)
		psl->size = psl->capacity = 0;
	//}
}

销毁顺序表时,释放arr数组空间,把size和capzcity置为0 即可。

5.打印顺序表中的数据

void SeqListPrint(SL* psl)
{
	assert(psl);//断言:防止psl为空

	//遍历数组
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->arr[i]);
	}

	printf("\n");

}

遍历数组,一次打印即可。

6.在头部插入数据

void SeqListPushFront(SL* psl, SLDataType x)
{
	assert(psl);//断言:防止psl为空

	CheckCapacity(psl);//检查容量,空间不够时扩容

	//从后往前挪动数据
	int end = psl->size - 1;
	while (end >= 0)
	{
		psl->arr[end + 1] = psl->arr[end];
		end--;
	}

	psl->arr[0] = x;
	psl->size++;

	//调用Insert函数
	//SLInsert(psl, 0, x);
}

在头部插入数据时,需要所有数据从后往前依次挪到下一个位置。(注意是从后往前挪,否则数据会被覆盖)

7.在尾部插入数据

void SeqListPushBack(SL* psl, SLDataType x)
{
	assert(psl);//断言:防止psl为空

	CheckCapacity(psl);//检查容量,空间不够时扩容

	psl->arr[psl->size] = x;
	psl->size++;

	//调用Insert函数
	//SLInsert(psl, psl->size, x);
}

在尾部插入时,直接插入即可。

8.在pos位置插入数据

void SeqListInsert(SL* psl, size_t pos, SLDataType x)
{
	assert(psl);//断言:防止psl为空
	assert(pos <= psl->size);//不能在psl->size之后插入

	CheckCapacity(psl);//检查容量,空间不够时扩容

	//int end = psl->size - 1;
	//while (end >= (int)pos)  //会有隐式转换,把int转化为unsigned int,所以可以把pos强转成int
	//{
	//	psl->arr[end + 1] = psl->arr[end];
	//	end--;
	//}
	
	//psl->arr[pos] = x;
	//psl->size++;

	size_t end = psl->size; //这样可以避免死循环的问题
	while (end > pos)
	{
		psl->arr[end] = psl->arr[end - 1];
		end--;
	}

	psl->arr[pos] = x;
	psl->size++;

}

在pos位置插入数据时,需要把pos后面的数据依次往后挪动一位。

9.在头部删除数据

void SeqListPopFront(SL* psl)
{
	assert(psl);//断言:防止psl为空

	//从前往后挪动数据
	/*int begin = 0;
	while (begin < psl->size - 1)
	{
		psl->arr[begin] = psl->arr[begin + 1];
		begin++;
	}
	psl->size--;*/

	int begin = 1;
	while (begin < psl->size)
	{
		psl->arr[begin - 1] = psl->arr[begin];
		begin++;
	}
	psl->size--;

	//调用SeqListErase函数
	//SeqListErase(psl, 0);
}

在头部删除数据时,需要将数据从前往后挪动一位**(注意是从前往后挪动,否则其他数据会被覆盖)**

10.在尾部删除数据

void SeqListPopBack(SL* psl)
{
	assert(psl);//断言:防止psl为空

	//调用SeqListErase函数
	//SeqListErase(psl, psl->size - 1);

	//如果尾部有数据,直接让psl->size--即可
	psl->size--;
}

如果尾部有数据,size–即可。

11.删除pos位置的数据

void SeqListErase(SL* psl, size_t pos)
{
	assert(psl);//断言:防止psl为空

	int end = pos;
	while (end < psl->size - 1)
	{
		psl->arr[end] = psl->arr[end + 1];
		end++;
	}
	psl->size--;

	//调用SeqListErase函数
	//SeqListErase(psl, pos);
}

把pos位置后面的数据依次往前挪动即可。

12.查找数据

int SeqListFind(SL* psl, SLDataType x)
{
	assert(psl);//断言:防止psl为空

	//遍历查找
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		if (x == psl->arr[i])
		return i;
	}

	return -1;//没有找到返回-1
}

13.修改pos 位置的数据

void SeqListModify(SL* psl, size_t pos, SLDataType x)
{
	assert(psl);//断言:防止psl为空
	assert(pos <= psl->size);//删除数据的位置要小于psl>size

	psl->arr[pos] = x;//修改数据
}

面试题:删除数据需要扩容吗?

我们知道,在插入数据时,在空间不够的时候,我们需要进行扩容,那么,我们在删除数据之后需要把空间返还给内存(缩容)吗?答案是不会。原因如下:

第一:我们缩容之后插入数据有需要重新进行扩容,我们知道,ralloc函数扩容是有代价的:扩容分为原地扩容和异地扩容。

原地扩容:当原来的空间后面有足够的可利用空间时,操作系统会直接把那一块空间交给我们使用,这种情况对效率的影响不大;

异地扩容:当原来空间后面没有足够的可利用的空间时,操作系统会在另外足够的地方为我们开辟一块新的空间,再把我们原来空间中的数据拷贝到新的空间中,再把原来的空间释放掉,这种情况对效率的影响就比较大了。

所以,频繁的扩容使用效率损失的。

第二:缩容也是有代价的,因为缩容和扩容的过程是一样的,分为原地扩和异地扩,会对程序的效率造成一定的影响。

第三:顺序表的申请是连续的一块空间,而free函数不能释放申请空间的一部分,只能一起释放。这就好比,我们团购优惠住酒店,你说你不住了,那时候就不能享受优惠了,因为团购才能有优惠,所以只能同时住和不住。在内存中申请空间和释放空间也是如此,所以我们做不到只释放一部分空间。

四、完整代码

1.SeqList.h

#define _CRT_SECURE_NO_WARNINGS 1

#pragma once      //防止头文件被重复包含

//#ifndef __SEQLIST_H__
//#define _SEQLIST_H__
// ......
//#endif

//包含头文件
#include 
#include 
#include 

//结构的定义和符号的表示
//静态顺序表
//#define N 100 //数组的最大长度
//typedef int SLDataType; //重命名数据类型
//struct SeqList
//{
//	SLDataType arr[N];    //使用定长数组来存储数据
//	int size;            //存储数据的个数
//}SL;


//动态顺序表
typedef int SLDataType;//将数据类型重命名为SLDataType
typedef struct SeqList
{
	SLDataType* arr;      //存储空间的地址
	int size;             //存储数据的个数
	int capacity;         //存储空间的大小
}SL;                      //重定义为SL


//函数的声明(函数接口)

//顺序表的初始化
void SeqListInit(SL* psl);
//检查顺序表的容量
void CheckCapacity(SL* psl);
//顺序表的销毁
void SeqListDestroy(SL* psl);
//打印顺序表中的数据
void SeqListPrint(SL* psl);
//在头部插入数据
void SeqListPushFront(SL* psl, SLDataType x);
//在尾部插入数据
void SeqListPushBack(SL* psl, SLDataType x);
//在pos位置插入数据
void SeqListInsert(SL* psl, size_t pos, SLDataType x);
//在头部删除数据
void SeqListPopFront(SL* psl);
//在尾部删除数据
void SeqListPopBack(SL* psl);
//删除pos位置的数据
void SeqListErase(SL* psl, size_t pos);
//查找数据     没有找到就返回-1
int SeqListFind(SL* psl, SLDataType x);
//修改pos位置数据
void SeqListModify(SL* psl, size_t pos, SLDataType x);

2.SeqList.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "SeqList.h"

//顺序表的初始化
void SeqListInit(SL* psl)
{
	assert(psl);//断言:防止psl为空


	psl->arr = NULL;
	psl->size = psl->capacity = 0;
}

//检查顺序表的容量(扩容)
//空间为0时开辟4个字节的空间,否则扩容2倍
void CheckCapacity(SL* psl)
{
	//检查容量
	if (psl->size == psl->capacity)
	{
		//将realloc的返回值由一个临时变量保存,防止扩容失败后丢失原来的空间地址
		int newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
		SLDataType* tmp = (int*)realloc(psl->arr, newCapacity * sizeof(SLDataType));
		if (tmp == NULL)//判空
		{
			perror("realloc fail");//打印错误信息
			return;
			//exit(-1);//退出程序
		}

		psl->arr = tmp;//开辟的空间地址赋给psl->arr
		psl->capacity = newCapacity;//新的空间大小
	}
}

//顺序表的销毁
void SeqListDestroy(SL* psl)
{
	assert(psl);//断言:防止psl为空

	/*if (psl->arr)
	{*/
		free(psl->arr); //释放(避免内存泄漏)
		psl->arr = NULL;//指针置空(避免野指针)
		psl->size = psl->capacity = 0;
	//}
}

//打印顺序表中的数据
void SeqListPrint(SL* psl)
{
	assert(psl);//断言:防止psl为空

	//遍历数组
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->arr[i]);
	}

	printf("\n");

}

//在头部插入数据
void SeqListPushFront(SL* psl, SLDataType x)
{
	assert(psl);//断言:防止psl为空

	CheckCapacity(psl);//检查容量,空间不够时扩容

	//从后往前挪动数据
	int end = psl->size - 1;
	while (end >= 0)
	{
		psl->arr[end + 1] = psl->arr[end];
		end--;
	}

	psl->arr[0] = x;
	psl->size++;

	//调用Insert函数
	//SLInsert(psl, 0, x);
}

//在尾部插入数据
void SeqListPushBack(SL* psl, SLDataType x)
{
	assert(psl);//断言:防止psl为空

	CheckCapacity(psl);//检查容量,空间不够时扩容

	psl->arr[psl->size] = x;
	psl->size++;

	//调用Insert函数
	//SLInsert(psl, psl->size, x);
}

//在pos位置插入数据
void SeqListInsert(SL* psl, size_t pos, SLDataType x)
{
	assert(psl);//断言:防止psl为空
	assert(pos <= psl->size);//不能在psl->size之后插入

	CheckCapacity(psl);//检查容量,空间不够时扩容

	//int end = psl->size - 1;
	//while (end >= (int)pos)  //会有隐式转换,把int转化为unsigned int,所以可以把pos强转成int
	//{
	//	psl->arr[end + 1] = psl->arr[end];
	//	end--;
	//}
	
	//psl->arr[pos] = x;
	//psl->size++;

	size_t end = psl->size; //这样可以避免死循环的问题
	while (end > pos)
	{
		psl->arr[end] = psl->arr[end - 1];
		end--;
	}

	psl->arr[pos] = x;
	psl->size++;

}

//在头部删除数据
void SeqListPopFront(SL* psl)
{
	assert(psl);//断言:防止psl为空

	//从前往后挪动数据
	/*int begin = 0;
	while (begin < psl->size - 1)
	{
		psl->arr[begin] = psl->arr[begin + 1];
		begin++;
	}
	psl->size--;*/

	int begin = 1;
	while (begin < psl->size)
	{
		psl->arr[begin - 1] = psl->arr[begin];
		begin++;
	}
	psl->size--;

	//调用SeqListErase函数
	//SeqListErase(psl, 0);
}

//在尾部删除数据
void SeqListPopBack(SL* psl)
{
	assert(psl);//断言:防止psl为空

	//调用SeqListErase函数
	//SeqListErase(psl, psl->size - 1);

	//如果尾部有数据,直接让psl->size--即可
	psl->size--;
}

//删除pos位置的数据
void SeqListErase(SL* psl, size_t pos)
{
	assert(psl);//断言:防止psl为空

	int end = pos;
	while (end < psl->size - 1)
	{
		psl->arr[end] = psl->arr[end + 1];
		end++;
	}
	psl->size--;

	//调用SeqListErase函数
	//SeqListErase(psl, pos);
}

//查找数据     没有找到就返回-1
int SeqListFind(SL* psl, SLDataType x)
{
	assert(psl);//断言:防止psl为空

	//遍历查找
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		if (x == psl->arr[i])
		return i;
	}

	return -1;//没有找到返回-1
}

//修改pos位置数据
void SeqListModify(SL* psl, size_t pos, SLDataType x)
{
	assert(psl);//断言:防止psl为空
	assert(pos <= psl->size);//删除数据的位置要小于psl>size

	psl->arr[pos] = x;//修改数据
}

3.test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "SeqList.h"

//测试插入数据
void TestSeqList1()
{
	SL sl;
	SeqListInit(&sl);

	//在头部插入数据
	SeqListPushFront(&sl, 1);
	SeqListPushFront(&sl, 2);
	SeqListPushFront(&sl, 3);
	SeqListPushFront(&sl, 4);

	SeqListPrint(&sl);

	//在尾部插入数据
	SeqListPushBack(&sl, 5);
	SeqListPushBack(&sl, 6);

	SeqListPrint(&sl);

	//在pos位置插入数据
	SeqListInsert(&sl, 0, 7);

	SeqListPrint(&sl);
}

//测试删除数据
void TestSeqList2()
{
	SL sl;
	SeqListInit(&sl);

	//在头部插入数据
	SeqListPushFront(&sl, 1);
	SeqListPushFront(&sl, 2);
	SeqListPushFront(&sl, 3);
	SeqListPushFront(&sl, 4);
	SeqListPushFront(&sl, 5);
	SeqListPushFront(&sl, 6);

	SeqListPrint(&sl);

	//删除头部数据

	SeqListPopFront(&sl);
	SeqListPopFront(&sl);

	SeqListPrint(&sl);

	//删除尾部数据
	SeqListPopBack(&sl);

	SeqListPrint(&sl);

	//删除pos位置数据
	SeqListErase(&sl, 0);

	SeqListPrint(&sl);

	//查找数据
	int ret = SeqListFind(&sl, 2);
	printf("%d\n", ret);

	//修改pos位置的数据为10
	SeqListModify(&sl, 1,10);

	SeqListPrint(&sl);

}


int main()
{
	//TestSeqList1();
	TestSeqList2();
	return 0;
}

五、顺序表的缺陷

顺序表存在以下缺陷:

\1. 中间/头部的插入删除,时间复杂度为O(N)

\2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。

\3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到

200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

你可能感兴趣的:(数据结构与算法,编程语言,C语言,数据结构,c语言,算法)