【数据结构手札】顺序表实战指南(三):扩容 | 尾插 | 尾删


🌈个人主页:聆风吟
🔥系列专栏:数据结构手札
🔖少年有梦不应止于心动,更要付诸行动。


文章目录

  • 📚专栏订阅推荐
  • [📋前言 - 顺序表文章合集](#📋前言 - 顺序表文章合集)
  • [一. ⛳️顺序表:重点回顾](#一. ⛳️顺序表:重点回顾)
    • [1.1 🔔顺序表的定义](#1.1 🔔顺序表的定义)
    • [1.2 🔔顺序表的分类](#1.2 🔔顺序表的分类)
      • [1.2.1 👻静态顺序表](#1.2.1 👻静态顺序表)
      • [1.2.2 👻动态顺序表](#1.2.2 👻动态顺序表)
  • [二. ⛳️顺序表的基本操作实现](#二. ⛳️顺序表的基本操作实现)
    • [2.1 🔔扩容](#2.1 🔔扩容)
    • [2.2 🔔尾插](#2.2 🔔尾插)
    • [2.3 🔔尾删](#2.3 🔔尾删)
  • [三. ⛳️顺序表的源代码](#三. ⛳️顺序表的源代码)
    • [3.1 🔔SeqList.h 顺序表的函数声明](#3.1 🔔SeqList.h 顺序表的函数声明)
    • [3.2 🔔SeqList.c 顺序表的函数定义](#3.2 🔔SeqList.c 顺序表的函数定义)
    • [3.3 🔔test.c 顺序表功能测试](#3.3 🔔test.c 顺序表功能测试)
  • 📝全文总结

📚专栏订阅推荐

专栏名称 专栏简介
数据结构手札 本专栏主要是我的数据结构入门学习手札,记录个人从基础到进阶的学习总结。
数据结构手札・刷题篇 本专栏是《数据结构手札》配套习题讲解,通过练习相关题目加深对算法理解。

📋前言 - 顺序表文章合集

-【顺序表实战指南(一):线性表定义 | 顺序表定义】
-【顺序表实战指南(二):结构体构建 | 初始化 | 打印 | 销毁】
-【顺序表实战指南(三):扩容 | 尾插 | 尾删】

后续文章会陆续补充,尽情期待...


一. ⛳️顺序表:重点回顾

1.1 🔔顺序表的定义

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

1.2 🔔顺序表的分类

顺序表一般可以分为:静态顺序表动态顺序表

1.2.1 👻静态顺序表

**静态顺序表:指存储空间是固定的并且在程序运行前就已经确定大小的顺序表。**它通常使用数组来实现,即通过定义一个固定长度的数组来存储数据元素。

静态顺序表的结构定义:

cpp 复制代码
//静态顺序表结构定义
#define MAXSIZE 7//存储单元初始分配量
typedef int SLDataType;//类型重命名,便于统一修改元素类型

typedef struct SeqList
{
	SLDataType data[MAXSIZE];//定长数组
	int size;//当前有效数据的个数
}SeqList;

我们可以发现描述静态顺序表需要三个属性:

  • 存储空间的起始位置:数组data,他的存储位置就是存储空间的存储位置;
  • 线性表的最大存储容量:数组长MAXSIZE
  • 线性表的当前位置:size

1.2.2 👻动态顺序表

**动态顺序表:通过动态分配内存空间,实现随着数据量的增加而不断扩容的效果。**它的结构类似于一个数组,数据元素的存储是连续的,支持随机访问和顺序访问。

动态顺序表的结构定义:

cpp 复制代码
//动态顺序表结构定义
typedef int SLDataType;//类型重命名,便于统一修改元素类型

typedef struct SeqList
{
	SLDataType* a;//指向动态开辟的数组
	int size;//当前有效数据的个数
	int capacity;//当前分配的总容量
}SL;

我们可以发现描述动态顺序表也需要三个属性:

  • 存储空间的起始位置:指针a,他里面存储的地址就是存储空间的地址;
  • 线性表当前最大存储容量:capacity,可以通过动态分配的方式进行扩容;
  • 线性表的当前位置:size

二. ⛳️顺序表的基本操作实现

通过上期学习,我们已经重点了解了动态顺序表 的结构体构建,并实现了初始化顺序表打印顺序表销毁顺序表 三大基础操作。本文我将继续讲解顺序表的扩容尾插尾删操作。

2.1 🔔扩容

因为扩容在尾插、头插以及在pos位置插入都需要使用,因此我们可以把扩容单独封装成一个函数,可以降低代码的的冗余。
整体思路图解:

cpp 复制代码
//检查容量是否够,不够进行扩容
void SLCheckCapacity(SL* ps)
{
	//断言检查指针有效性
	assert(ps);

	//判断是否需要扩容
	if (ps->size == ps->capacity)
	{
		//使用realloc进行扩容
		SLDataType* temp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * 2 * (ps->capacity));
		//检查是否扩容成功
		if (temp == NULL)
		{
			perror("realloc failed");//打印错误原因(如内存不足)
			exit(-1);//终止程序,避免后续非法操作
		}

		ps->a = temp;//更新数组指针
		ps->capacity *= 2;//更新容量值
	}
}

代码深剖:
(1)realloc 函数

realloc全称是 "reallocate memory",直译就是 "重新分配内存"。它的作用是:修改之前通过 malloc/calloc/realloc 分配的内存块的大小,并返回调整后内存块的首地址。

函数原型:

cpp 复制代码
//头文件
#include<stdlib.h>
//原型
void *realloc(void *ptr, size_t new_size);
  • ptr :指向原有内存块的指针(如果传 NULLrealloc 等价于 malloc(new_size));
  • new_size :调整后内存块的新大小(单位:字节);
  • 返回值 :如果分配成功,则返回调整后内存块的首地址;如果分配失败:返回 NULL

(2)代码中 realloc 参数解析

  • 第一个参数 ps->a:指向原有内存块的指针(顺序表的数组指针);
  • 第二个参数 sizeof(SLDataType) * 2 * ps->capacity:扩容后的总字节数 ------ 容量翻倍(2*capacity),再乘以单个元素的字节大小(sizeof(SLDataType)),保证字节数计算正确。

2.2 🔔尾插

尾插时需要先判断顺序表是否满了,满了要先进行扩容才能继续进行插入操作。size表示有效元素个数,由于数组下标从0开始,因此size同时也是顺序表中最后一个元素后一个位置的下标。成功插入后要对有效数据个数size进行加1操作。
整体思路图解:

cpp 复制代码
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
	//断言检查指针有效性
	assert(ps);

	//检查是否需要扩容
	SLCheckCapacity(ps);
	
	ps->a[ps->size] = x;//尾插核心操作:赋值
	ps->size++;//更新已用元素个数
}

代码深剖:
(1)ps->a[ps->size] = x;

结合上面尾插图,ps->size 是已存储的元素个数,数组下标从 0 开始,因此最后一个元素的下标是 size-1size 就是尾部的 "空闲位置下标"。

(2)ps->size++;

  • 作用:插入元素后,已存储的元素个数加 1,为下一次尾插 / 遍历 / 其他操作提供正确的位置依据。
  • 易错点 :如果漏写这一行,会导致后续尾插覆盖当前插入的元素(因为 size 未更新,下次仍会插入到同一个位置),且顺序表的元素个数统计错误。

时间复杂度:

因为不需要移动元素,直接操作 size 位置,根据大O阶的推导方法很容易得出:尾插的时间复杂度为O(1)

2.3 🔔尾删

尾删时需要先判断顺序表是否为空(无有效元素可删),空表删除会导致非法访问,必须先拦截。size表示有效元素个数,最后一个有效元素的下标为size-1,只需将size减 1,即可实现 "逻辑删除"(无需修改元素值或释放内存,物理内存仍存在,仅通过size的调整将原最后一个元素标记为无效),后续操作不再访问原最后一个元素,等同于完成删除。
整体思路图解:

cpp 复制代码
//尾删
void SLPopBack(SL* ps)
{
	//检查顺序表指针的有效性
	assert(ps);

	//温柔检查
	/*if (ps->size == 0)
		return;*/
		
	//暴力检查:检查是否有元素可删
	assert(ps->size > 0);
	
	ps->size--;//尾删的核心操作:逻辑删除
}

代码深剖:
检查顺序表是否为空的方法

在代码中我们提供两种检查顺序表是否为空的办法。第一种是比较温柔的检查,如果顺序表为空直接返回,返回之后仍然可以进行其他操作。第二种是比较暴力的检查方法,直接提示错误并打印出错误位置的行号。

时间复杂度:

因为不需要移动元素,直接操作 size 位置,根据大O阶的推导方法很容易得出:尾删的时间复杂度为O(1)

三. ⛳️顺序表的源代码

3.1 🔔SeqList.h 顺序表的函数声明

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

//动态顺序表
#define SLCAPACITY 4
typedef int SLDataType;

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

//******************** 本文最新学习内容 ********************
//检查容量是否够,不够进行扩容
void SLCheckCapacity(SL* ps);
//尾插
void SLPushBack(SL* ps, SLDataType x);
//尾删
void SLPopBack(SL* ps);

//******************** 上期学习内容:可能会调用 ********************
//初始化
void SLInit(SL* ps);
//销毁顺序表
void SLDestroy(SL* ps);
//打印顺序表
void SLPrint(SL* ps);

3.2 🔔SeqList.c 顺序表的函数定义

cpp 复制代码
#include "SeqList.h"

//******************** 本文最新学习内容 ********************
//检查容量是否够,不够进行扩容
void SLCheckCapacity(SL* ps)
{
	//断言检查指针有效性
	assert(ps);

	//判断是否需要扩容
	if (ps->size == ps->capacity)
	{
		//使用realloc进行扩容
		SLDataType* temp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * 2 * (ps->capacity));
		//检查是否扩容成功
		if (temp == NULL)
		{
			perror("realloc failed");//打印错误原因(如内存不足)
			exit(-1);//终止程序,避免后续非法操作
		}

		ps->a = temp;//更新数组指针
		ps->capacity *= 2;//更新容量值
	}
}

//尾插
void SLPushBack(SL* ps, SLDataType x)
{
	//断言检查指针有效性
	assert(ps);

	//检查是否需要扩容
	SLCheckCapacity(ps);
	
	ps->a[ps->size] = x;//尾插核心操作:赋值
	ps->size++;//更新已用元素个数
}

//尾删
void SLPopBack(SL* ps)
{
	//检查顺序表指针的有效性
	assert(ps);
		
	//暴力检查:检查是否有元素可删
	assert(ps->size > 0);
	
	ps->size--;//尾删的核心操作:逻辑删除
}

//******************** 上期学习内容:可能会调用 ********************
//初始化顺序表
void SLInit(SL* ps)
{
	assert(ps);
	//使用malloc开辟空间
	ps->a = (SLDataType*)malloc(sizeof(SLDataType) * SLCAPACITY);
	//判断空间是否开辟成功
	if (NULL == ps->a)
	{
		//打印错误原因(比如 "malloc failed: Out of memory"),方便定位问题;
		perror("malloc failed");
		//终止程序并返回非 0 状态码(约定俗成表示程序异常退出),避免后续无效操作。
		exit(-1);
	}
	
	//初始化顺序表的有效元素个数为 0。
	ps->size = 0;
	//记录顺序表当前的最大容量。
	ps->capacity = SLCAPACITY;
}

//销毁顺序表
void SLDestroy(SL* ps)
{
	assert(ps);
	
	//释放顺序表底层数组占用的动态内存。
	free(ps->a);
	//将指针置空,避免 "野指针" 问题。
	ps->a = NULL;
	//重置顺序表的状态变量,让其回归 "初始无效状态"。
	ps->size = ps->capacity = 0;
}

//打印顺序表
void SLPrint(SL* ps)
{
	assert(ps);

	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->a[i]);
	}
	printf("\n");
}

3.3 🔔test.c 顺序表功能测试

c 复制代码
#include "SeqList.h"

int main()
{
	SL sl;
	//初始化顺序表:上节课已讲过,本文不做过多叙述
	SLInit(&sl);

	printf("******************** 尾插操作 ********************\n");
	//尾插
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPushBack(&sl, 3);
	SLPushBack(&sl, 4);
	SLPushBack(&sl, 5);
	//打印顺序表:上节课已讲过,本文不做过多叙述
	SLPrint(&sl);

	printf("******************** 尾删操作 ********************\n");
	//尾删
	SLPopBack(&sl);
	SLPopBack(&sl);
	SLPopBack(&sl);
	//打印顺序表:上节课已讲过,本文不做过多叙述
	SLPrint(&sl);
	
    //销毁顺序表:上节课已讲过,本文不做过多叙述
    SLDestroy(&sl);
    return 0;
}

代码运行图:


📝全文总结

本文重点讲解顺序表的扩容尾插尾删 三大基础操作,下期我们将继续讲解顺序表的头插头删等操作。

今天的干货分享到这里就结束啦!如果觉得文章还可以的话,希望能给个三连支持一下,聆风吟的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是作者前进的最大动力!

相关推荐
IT方大同2 小时前
数组的初始化与使用
c语言·数据结构·算法
im_AMBER2 小时前
Leetcode 84 水果成篮 | 删除子数组的最大得分
数据结构·c++·笔记·学习·算法·leetcode·哈希算法
AAA阿giao2 小时前
从树到楼梯:数据结构与算法的奇妙旅程
前端·javascript·数据结构·学习·算法·力扣·
Sheep Shaun2 小时前
STL:list,stack和queue
数据结构·c++·算法·链表·list
_OP_CHEN2 小时前
【C++数据结构进阶】吃透 LRU Cache缓存算法:O (1) 效率缓存设计全解析
数据结构·数据库·c++·缓存·线程安全·内存优化·lru
white-persist2 小时前
【攻防世界】reverse | tt3441810 详细题解 WP
java·c语言·开发语言·数据结构·c++·算法·安全
量子炒饭大师2 小时前
Cyber骇客的树状逻辑数据——【初阶数据结构与算法】树
c语言·数据结构·c++·二叉树·
lxh01132 小时前
缺失的第一个正数
数据结构·算法
杨福瑞2 小时前
数据结构:⼆叉树(1)
c语言·数据结构