【极简数据结构】快速了解并实现顺序表,速通玩家的最爱

前言

从0快速实现顺序表。


一、线性表

线性表(Linear list)是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表有:顺序表、链表、栈、队列、字符串…
② 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物 理上存储时,通常以数组和链式结构的形式存储。

在这里插入图片描述


二、顺序表

1.顺序表的概念

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。

💡顺序表顾名思义,数据是按着顺序连续存储的。
顺序表分为

  1. 静态顺序表

💬 使用定长数组存储元素:

#define N 7
typedef int SLDataType;
typedef struct SeqList
{
    SLDataType array[N];  //定长数组
    size_t size;          //有效数据的个数
} SL;

在这里插入图片描述

📚静态顺序表的特点:如果满了就不让插入
🆘静态顺序表的缺点:设置的长度是多少呢?不能准确确定。N给小了不够用;N给大了就显得浪费

  1. 动态顺序表

💬 使用动态开辟的数组存储元素:

typedef int SLDataType;
 
typedef struct SeqList
{
    SLDataType* array;    //指向动态开辟的数组
    size_t size;          //有效数据的个数
    size_t capacity;      //容量空间的大小
} SL;

在这里插入图片描述

动态顺序表可根据需要动态地分配空间,因此在现实中广泛使用。


2.接口函数

接口函数: 某个模块写了(主要)给其它模块用的函数。
📌注意:
① 接口函数的命名风格跟着STL走,便于后期学习STL
② 模块化思想:SeqList.h 头文件存放 结构体、函数的声明; SeqList.c 源文件存放 函数的定义;Test.c 源文件 用于测试。

💬主要的接口函数有:
接口函数采用址传递方式

📁SeqList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

//动态顺序表
typedef int SLDataType;
typedef struct SeqList
{
    SLDataType* array;    //指向动态开辟的数组
    size_t size;          //有效数据的个数
    size_t capacity;      //容量空间的大小
} SL;

/*接口函数*/
//顺序表 初始化
void SeqListInit(SL* psl);
//顺序表 尾插
void SeqListPushBack(SL* psl, SLDateType x);
//顺序表 打印
void SeqListPrint(SL* psl);
//顺序表 销毁
void SeqListDestory(SL* psl);
//顺序表 尾删
void SeqListPopBack(SL* psl);
//顺序表 头插
void SeqListPushFront(SL* psl,SLDateType x);
//顺序表 判断扩容
void SeqListCheckCapacity(SL* psl);
//顺序表 头删
void SeqListPopFront(SL* psl);
//顺序表 查找
int SeqListFind(SL* psl,SLDateType x);
//顺序表 任意pos位置插入
int SeqListInsert(SL* psl, int pos, SLDateType x);
//顺序表 任意pos位置删除
void SeqListEarse(SL* psl, int pos);

🔑解读:#pragma once 为了避免同一个头文件被多次包含。

顺序表 初始化

📁SeqList.c
//顺序表 初始化
void SeqListInit(SL* psl)
{
	psl->array = NULL;
	psl->size = psl->capacity = 0;
}

🔑解读:

顺序表 尾插

💡尾插要考虑三种情况:

  1. 整个顺序表没有空间
  2. 空间已满,则扩容
  3. 空间是足够的,直接插入数据即可

🔱总结: 尾插需要考虑是否需要扩容

📁SeqList.c
//顺序表 尾插
void SeqListPushBack(SL* psl, SLDataType x)
{		
	//判断是否需要扩容
	if (psl->size == psl->capacity)
	{
		int newcapacity = psl->capacity == 0 ? 4 : psl->capacity*2;
		//realloc:若原空间为空,就等于malloc。调整为newcapacity个SLDataType大小的空间
		SLDataType* tmp = (SLDataType*)realloc(psl->array, new_capacity * sizeof(SLDataType));
        //防止realloc翻车,也可以用assert(tmp);断言
		if (tmp == NULL)
		{
			printf("realloc failed!\n");
			exit(-1);
		}
		//更新
		psl->array = tmp;
		psl->capacity = newcapacity;
	}
	//插入
	psl->array[psl->size] = x;
	psl->size++;
}

🔑解读:
① 扩容时,扩大为原来capacity的2倍(2倍是比较合理、折中的倍数,你也可以选择其他的倍数)
② 为了避免realloc失败返回NULL导致后续会访问空,我们创建了临时指针tmp接收,并对其判断。

顺序表 打印

📁SeqList.c
//顺序表 打印
void SeqListPrint(SL* psl)
{
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->array[i]);
	}
	printf("\n");
}

🔑解读: for循环遍历顺序表,并逐一打印。

📌温馨提示:编写代码要养成写一段测试一段的习惯。切勿等写完再来测试

测试代码:

📁Test.c
#include "SeqList.h"
void TestSeqList1()
{
	SL sl;
	SeqListInit(&sl);
 
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	SeqListPushBack(&sl, 5);
	SeqListPrint(&SL);
}
 
int main() 
{
	TestSeqList1();
	return 0;
}

在这里插入图片描述
🔑解读: TestSeqList1();模块化测试思想
具体测试方法:【实用调试技巧】,在此不再叙述。一定要多尝试自己测试调试代码。(下文就不再带大家测试了)

顺序表 销毁

📌注意: 数组是动态开辟的,对于动态开辟的内存空间,我们要养成手动free掉的习惯,否则会有内存泄露的风险。

📁SeqList.c
//顺序表 销毁
void SeqListDestory(SL* psl)
{
	free(psl->array);
	psl->array = NULL;
	psl->capacity = psl->size = 0;
}

🔑解读: free掉psl->array指向的内存空间后,要psl->array置空,以防止其变成野指针。

顺序表 尾删

① 尾删就是让psl->size--;(让有效数据减1即可,因为psl->size作为数组下标时是下一个插入数据的位置)
在这里插入图片描述

② 尾删要考虑顺序表是否为空的情况,当顺序表都空了还删除会有越界非法访问的安全问题。即尾删要考虑判空

📁SeqList.c
//顺序表 尾删
void SeqListPopBack(SL* psl)
{
	//1.温柔手法
	/*	if (psl->size > 0)
	{
		psl->size--;
	}*/
	//2.暴力手法
	assert(psl->size > 0);
	psl->size--;
}

🔑解读: 尾插要考虑判空,判空建议用断言方式。

顺序表 头插 和 顺序表 扩容

📁SeqList.c
//顺序表 扩容
void SeqListCheckCapacity(SL* psl)
{
	//判断是否需要扩容
	if (psl->size == psl->capacity)
	{
		int newcapacity = psl->capacity == 0 ? 4 : psl->capacity*2;
		//realloc:若原空间为空,就等于malloc。调整为newcapacity个SLDataType大小的空间
		SLDataType* tmp = (SLDataType*)realloc(psl->array, new_capacity * sizeof(SLDataType));
        //防止realloc翻车,也可以用assert(tmp);断言
		if (tmp == NULL)
		{
			printf("realloc failed!\n");
			exit(-1);
		}
		//更新
		psl->array = tmp;
		psl->capacity = newcapacity;
	}
}
//顺序表 头插
void SeqListPushFront(SL* psl, SLDateType x)
{
	SeqListCheckCapacity(psl);//复用
	int end = psl->size - 1;//psl->size作为数组下标时是下一个插入数据的位置,所以要减1
	while (end >= 0)
	{
		psl->array[end + 1] = psl->array[end];
		end--;
	}
	//此时,array[0]被挪到后一位
	psl->array[0] = x;
	psl->size++;
}

🔑解读:

插入要考虑是否需要扩容,头插和尾插都需要扩容,我们可以将扩容操作封装为一个SeqListCheckCapacity函数,再在头插和尾插中复用。
② 顺序表的数据是从头开始连续存储的。所以顺序表实现头插,需要将所有元素往后挪动1位。int end = psl->size - 1; 按psl->array[end]、psl->array[end - 1]…的顺序依次往后挪1位

⚡优化顺序表 尾删

在尾插中复用扩容

📁SeqList.c
//顺序表 尾插
void SeqListPushBack(SL* psl, SLDateType x)
{
	SeqListCheckCapacity(psl);//复用扩容函数
	psl->array[psl->size] = x;
	psl->size++;
}

优化后可读性提高!

顺序表 头删

📁SeqList.c
//顺序表 头删
void SeqListPopFront(SL* psl)
{
	assert(psl->size > 0);
	int begin = 1;
	while (begin < psl->size)
	{
		psl->array[begin - 1] = psl->array[begin];
		begin++;
	}
	psl->size--;
}

🔑解读:
删除需要考虑判空
int begin = 1; 按psl->array[1]、psl->array[2]…的顺序依次往前挪1位,最后psl->size--;

顺序表 查找

📁SeqList.c
//顺序表 查找
int SeqListFind(SL* psl,SLDateType x)
{
	//数组下标从0开始
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		if (psl->array[i] == x)
		{
			return i;
		}
	}
	//没找到
	return -1;
}

🔑解读:
比较简单,for循环依次遍历顺序表 若 psl->array[i] == x ,则返回下标 i;没找到就返回 -1
时间复杂度为O(N)。可以尝试写一个二分查找算法的接口函数实现,但前提要排序。

顺序表 任意pos位置插入

前面学习了顺序表的头插和尾插,我们能不能在任意的pos位置插入呢?这得考虑考虑!

在这里插入图片描述
因此我们要对pos进行条件限定。另外,插入就得考虑是否需要扩容。

📁SeqList.c
//顺序表 任意pos位置插入
int SeqListInsert(SL* psl, int pos, SLDateType x)
{
	//插入的pos位置有区域约束

	//1.温柔方法
	/*	if (pos < 0 && pos > psl->size)
	{
		printf("pos invalidd(pos非法!)\n");
		return;
	}*/

	//2.暴力方法
	assert(pos >= 0 && pos <= psl->size);

	//凡是插入,考虑是否需要扩容
	SeqListCheckCapacity(psl);

	//挪数据
	int end = psl->size - 1;
	while (end >= pos)
	{
		psl->array[end + 1] = psl->array[end];
		end--;
	}

	//正式插入
	psl->array[pos] = x;
	psl->size++;
}

🔑解读:
思路类似头插,只是while循环的终止条件发生变化。

🔱对头插函数和尾插函数的优化

SeqListInsert函数可以在任意pos位置插入数据,当然包括了头部和尾部。因此我们可以在头插和尾插函数中复用SeqListInsert函数。真是妙极了!

头插函数复用SeqListInsert函数

//顺序表 头插(复用SeqListInsert函数)
void SeqListPushFront(SL* psl, SLDataType x) 
{
	SeqListInsert(psl, 0, x);
}

尾插函数复用SeqListInsert函数

//顺序表 尾插(复用SeqListInsert函数
void SeqListPushBack(SL* psl, SLDataType x) 
{
	SeqListInsert(psl, psl->size, x);
}

顺序表 任意pos位置删除

① 类似任意pos位置插入,我们也可以在pos位置删除。同样pos位置是有条件限定的,特别的是注意psl->size无有效数据,不可删除。

② 删除就得考虑判空

//顺序表 任意pos位置删除
void SeqListEarse(SL* psl, int pos)
{
	//删除和插入一样:有区域约束,不可任意访问删除
	  
	//1.温柔方法
	/*	if (pos < 0 && pos >= 0)
	{
		printf("pos invalidd!\n");
		return;
	}*/

	//2.暴力处理
	assert(pos >= 0 && pos < psl->size);

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

🔑解读:
思路类似头删。

🔱对头删函数和尾删函数的优化

同样的道理,头删和尾删也可以复用SeqListEarse函数
头删函数复用SeqListEarse函数

//顺序表 头删(复用SeqListEarse函数)
void SeqListPopFront(SL* psl) 
{
	SeqListEarse(psl, 0);
}

尾删函数复用SeqListEarse函数

//顺序表 尾删(复用SeqListEarse函数)
void SeqListPopBack(SL* psl)
{
	SeqListEarse(psl, psl->size - 1);
}

到此,想必速通万家们已经知道如何快速实现顺序表了!
我们把SeqListInsert函数和SeqListEarse函数写了,就相当于把头插尾插、头删尾删写了(复用即可)


四、完整代码

📁SeqList.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

创建静态顺序表
//#define N 10
//typedef int SLDateType;
//
//typedef struct SeqList
//{
//	SLDateType array[N];//定长数组
//	SLDateType size;//有效个数
//}SL;

//创建动态顺序表
typedef int SLDateType;
typedef struct SeqList
{
	SLDateType* array;//指向动态开辟的数组
	SLDateType size;//有效个数
	SLDateType capacity;//容量空间大小
}SL;

/*接口函数*/

//顺序表 初始化
void SeqListInit(SL* psl);

//顺序表 尾插
void SeqListPushBack(SL* psl, SLDateType x);

//顺序表 打印
void SeqListPrint(SL* psl);

//顺序表 销毁
void SeqListDestory(SL* psl);

//顺序表 尾删
void SeqListPopBack(SL* psl);

//顺序表 头插
void SeqListPushFront(SL* psl,SLDateType x);

//顺序表 判断扩容
void SeqListCheckCapacity(SL* psl);

//顺序表 头删
void SeqListPopFront(SL* psl);

//顺序表 查找
int SeqListFind(SL* psl,SLDateType x);

//顺序表 任意pos位置插入
int SeqListInsert(SL* psl, int pos, SLDateType x);

//顺序表 任意pos位置删除
void SeqListEarse(SL* psl, int pos);

📁SeqList.c

#include"SeqList.h"

//顺序表 初始化
void SeqListInit(SL* psl)
{
	psl->array = NULL;
	psl->size = psl->capacity = 0;
}

//顺序表 尾插
void SeqListPushBack(SL* psl, SLDateType x)
{
	//插入
	SeqListCheckCapacity(psl);
	psl->array[psl->size] = x;
	psl->size++;
}

//顺序表 打印
void SeqListPrint(SL* psl)
{
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->array[i]);
	}
	printf("\n");
}

//顺序表 销毁
void SeqListDestory(SL* psl)
{
	free(psl->array);
	psl->array = NULL;
	psl->capacity = psl->size = 0;
}

//顺序表 尾删
void SeqListPopBack(SL* psl)
{
	1.温柔手法
	//if (psl->size > 0)
	//{
	//	psl->size--;
	//}
	
	//没有考虑到"删过头"的情况,会非法访问
	psl->array[psl->size - 1] = 0;
	//psl->size--;

	//2.暴力手法
	//assert(psl->size > 0);
	//psl->size--;
	
	//复用SeqListEarse
	SeqListEarse(psl, psl->size - 1);
}

//顺序表 头插
void SeqListPushFront(SL* psl, SLDateType x)
{
	SeqListCheckCapacity(psl);
	int end = psl->size - 1;
	while (end >= 0)
	{
		psl->array[end + 1] = psl->array[end];
		end--;
	}

	//此时,array[0]被挪到后一位
	psl->array[0] = x;
	psl->size++;
}

//顺序表 判断扩容
void SeqListCheckCapacity(SL* psl)
{
	if (psl->size == psl->capacity)//判断空间是否足够
	{
		//扩容
		SLDateType newcapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
		SLDateType* tmp = (SLDateType*)realloc(psl->array, newcapacity * sizeof(SLDateType));;
		if (tmp == NULL)
		{
			printf("扩容失败!\n");
			exit(-1);
		}

		//扩容后需要更新的
		psl->array = tmp;
		psl->capacity = newcapacity;
	}
}

//顺序表 头删
void SeqListPopFront(SL* psl)
{
	//assert(psl->size > 0);
	//int begin = 1;
	//while (begin<psl->size)
	//{
	//	psl->array[begin - 1] = psl->array[begin];
	//	begin++;
	//}
	//psl->size--;

	//复用SeqListEarse
	SeqListEarse(psl, 0);
}

//顺序表 查找
int SeqListFind(SL* psl,SLDateType x)
{
	//数组下标从0开始
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		if (psl->array[i] == x)
		{
			return i;
		}
	}

	//没找到
	return -1;
}

//顺序表 任意pos位置插入
int SeqListInsert(SL* psl, int pos, SLDateType x)
{
	//插入有区域约束

	//温柔处理:
	/*	if (pos < 0 && pos > psl->size)
	{
		printf("pos invalidd(pos非法!)\n");
		return;
	}*/

	//暴力解决
	assert(pos >= 0 && pos <= psl->size);

	//凡是插入,考虑是否需要扩容
	SeqListCheckCapacity(psl);

	//挪数据
	int end = psl->size - 1;
	while (end>=pos)
	{
		psl->array[end + 1] = psl->array[end];
		end--;
	}

	//正式插入
	psl->array[pos] = x;
	psl->size++;
}

//顺序表 任意pos位置删除
void SeqListEarse(SL* psl, int pos)
{
	//删除和插入一样:有区域约束,不可任意访问删除
	  
	//温柔处理
	/*	if (pos < 0 && pos >= 0)
	{
		printf("pos invalidd(pos非法!)\n");
		return;
	}*/

	//暴力处理
	assert(pos >= 0 && pos < psl->size);

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

	psl->size--;
}

📁Test.c

#include"SeqList.h"

void TestSeqList1()
{
	SL sl;
	SeqListInit(&sl);
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	SeqListPushBack(&sl, 5);
	SeqListPrint(&sl);

	//SeqListPopBack(&sl);
	//SeqListPopBack(&sl);
	//SeqListPopBack(&sl);
	//SeqListPopBack(&sl);
	//SeqListPopBack(&sl);
	//SeqListPopBack(&sl);
	//SeqListPrint(&sl);

	//SeqListPushBack(&sl, 10);
	//SeqListPushBack(&sl, 20);
	//SeqListPrint(&sl);

	//SeqListPushFront(&sl, 1);
	//SeqListPushFront(&sl, 2);
	//SeqListPushFront(&sl, 3);
	//SeqListPushFront(&sl, 4);

	//SeqListPrint(&sl);

	//SeqListDestory(&sl);
}

void TestSeqList2()
{
	SL sl;
	SeqListInit(&sl);

	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);

	SeqListPrint(&sl);

	SeqListPushFront(&sl, 1);
	SeqListPushFront(&sl, 2);
	SeqListPushFront(&sl, 3);
	SeqListPushFront(&sl, 4);
	SeqListPrint(&sl);

	//SeqListDestory(&sl);
}

void TestSeqList3()
{
	SL sl;
	SeqListInit(&sl);
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);


	SeqListPrint(&sl);

	//int ret = SeqListFind(&sl, 3);
	//if (ret == -1)
	//	printf("找不到!\n");
	//else
	//	printf("找到了,下标为%d", ret);

	SeqListInsert(&sl, 5, 5);
	SeqListPrint(&sl);
	SeqListDestory(&sl);
}

void TestSeqList4()
{
	SL sl;
	SeqListInit(&sl);
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	SeqListPushFront(&sl, 1);
	SeqListPushFront(&sl, 2);
	SeqListPushFront(&sl, 3);
	SeqListPushFront(&sl, 4);

	SeqListPrint(&sl);
	SeqListPopFront(&sl);
	SeqListPopBack(&sl);
	SeqListPrint(&sl);

	SeqListDestory(&sl);
}

int main()
{
	//TestSeqList1();
	//TestSeqList2();
	//TestSeqList3();
	TestSeqList4();
}

在这里插入图片描述
简单的三连是对作者的最大支持!
请添加图片描述

✒️ 笔者:陈汉新

📅 更新: 2022.4.13

❌ 勘误: 暂无

📣 声明:由于作者水平有限,本文错误之处在所难免,敬请读者指正!

  • 139
    点赞
  • 99
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 122
    评论
评论 122
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大猩猩!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值