数据结构:线性表之顺序表

线性表

线性表(linear list)是相同类型的n个数据元素的有限序列,并用L命名为线性表,表示为:L={a1,a2,a3,......,an}。

ai是线性表中第i个数据元素,称i为数据元素ai在线性表中的位序(从1开始)。

需要注意的是,位序 i 是数据元素在顺序表中的位置,数组下标 i 则是相对于第一个数据元素的偏移量(从0开始)。

n是表的长度,当n=0时,则为空表。

a1为表头元素(表中第一个数据元素),an为表尾元素(表中最后一个数据元素)。

表中第一个数据元素没有前驱,最后一个元素没有后继。

线性表的两种实现方式

用顺序存储的方式实现就叫顺序表,用链式存储的方式实现就叫链表。

顺序存储:将逻辑上相邻的数据元素存放在一段连续的物理存储单元中,物理存储关系就可以表示数据元素之间的逻辑关系。

链式存储:把逻辑上相邻的梳理元素存储在任意一组物理存储单元中,数据元素之间的逻辑关系由指针表示。

顺序表

静态顺序表

静态顺序表就是用固定大小的静态数组来存储数据,优点是实现简单,缺点是适用场景局限,只适用于确定最多要存的数据个数,否则空间申请多了浪费,少了不够用。

静态顺序表只在确定要存的数据元素个数的时候才适用,适用场景太局限,所以我们一般用的是动态顺序表。

动态顺序表

动态顺序表就是用一个堆上动态申请开辟的内存空间来存储数据,容量不够了可以进行扩容处理。

实现动态顺序表

因为动态顺序表适用场景更广,也更复杂一点,所以这里我们只讲如何实现动态顺序表,这里用C语言来实现。

要实现动态顺序表,就要定义好它的接口函数(初始化、插入、删除等),

这些是它所有定义的接口函数的声明:

c 复制代码
#pragma once

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>

typedef int SqDatatype;//typedef重命名方便类型修改

//动态顺序表
typedef struct SequeenList
{
	SqDatatype* arr;//用于开辟动态数组
	int size;//表中数据元素个数
	int capacity;//容量大小
}SqList;
//将结构体类型struct SequeenList重命名为SqList
//方便后续创建顺序表

//初始化顺序表
void SqListInit(SqList* ps);

//返回顺序表中第i个下标数据元素的值
SqDatatype GetElem(SqList* ps, int i);

//在顺序表中查找元素x,找到则返回该数据元素的位置i
int LocateElem(SqList* ps, SqDatatype x);

//在顺序表第i个下标插入数据元素x
void SqListInsert(SqList* ps, int i, SqDatatype x);

//删除第i个下标的数据元素x,并返回删除的值
SqDatatype SqListDelete(SqList* ps, int i);

//获取顺序表中有效元素的个数
int SqListSize(SqList* ps);

//打印顺序表中的所有数据元素
void SqListPrint(SqList* ps);

//判断顺序表是否为空表,是则返回true,不是则返回false
bool EmptySqList(SqList* ps);

//头插和尾插
void SqListInsertFront(SqList* ps, SqDatatype x);
void SqListInsertBack(SqList* ps, SqDatatype x);

//头删和尾删
void SqListDeleteFront(SqList* ps);
void SqListDeleteBack(SqList* ps);

//销毁顺序表
void SqListDestroy(SqList* ps);

下面我们来讲讲如何实现主要的一些接口函数。注:以下位置 i 指的是数组下标。

初始化

初始化顺序表很简单,我们先保证传进来的指针ps的有效性,然后用SqList类型的中顺序表的数据元素类型SqDataType*的指针arr接收开辟的内存空间的起始地址(这里开辟能存放4个数据元素的长度),如果开辟成功则继续往下执行。再给顺序表中的数据元素个数size初始化成0,内存容量capacity初始化成用malloc开辟的内存容量就可以了。

查找

我们需要在顺序表中查找元素x出现的位置i,如果找到则返回下标i,遍历一遍顺序表的数据元素都没找到则返回-1。

插入

我们想在顺序表的任意地方都能插入数据元素,当我们在第i个下标插入数据元素x时,i后面的数据元素都要往后挪一个位置,并且是从后往前开始挪(防止前面的数据元素把后面的覆盖掉)。我这里的下标应该从0开始,我标错了。

为了使得插入这个数据元素后还是一个顺序表(相邻数据元素的物理位置连续),我们判断是否可以插入这个数据元素的条件应该设为要插入数据元素的位置i≤size(数据元素的个数)。如果我们的判定条件设为i<capacity(顺序表容量大小)。以上图为例,我们插入元素5到位置9的话,这就不再是一个顺序表了(逻辑关系错误,相邻数据元素存放位置不连续)。放了一个元素后size要+1(数据元素个数+1)。

这是在顺序表容量足够大的情况下的。当顺序表容量不够时,我们就需要对其进行扩充。

所以我们在判断是否可以插入前,应该先判断是否需要进行容量扩充(用realloc函数扩充)。

每次只扩充一个显然不合理,但在数据结构中也没有统一的标准,所以这里用realloc重新开辟内存为原来的两倍。

我们知道对于realloc扩容有两种情况,一种是原地往后扩容,另一种是新找一块足够大的空间,然后把数据元素都拷贝过来,再把旧内存释放掉。显然第二种情况耗费的时间更长,但具体是哪种情况我们是不可知也是不可控的,如果想知道可以通过调试观察指针str和指针ps存的是不是同一个地址来判断。

头插和尾插复用该函数就可以实现:

调试看是否成功插入:

插入成功,说明函数功能没有什么问题。

删除

与插入相对的,我们删除一个元素后要保证这个表还是一个顺序表。

这里就是从前往后挪数据元素了。

如图,显而易见,当我们传入的下标位置小于顺序表中已有数据元素的个数并且大于等于0时,才能有效执行删除指令。

同样的,删除一个元素后,size要-1(顺序表中已有数据元素的个数-1)。

头删尾删函数复用该函数就可:

销毁

销毁顺序表很简单,只要把动态开辟的空间释放,size和capacity置为0就可以了。

剩余接口函数代码

剩下的一些函数(如打印、判空等)实现很简单,这里就不详细说了,直接展示代码。

该表达式结果为真则返回true,为假则返回false,说明不是空表。

为什么要定义接口函数

看完所有的函数我们发现,有些函数功能就是一些常规的操作,比如说访问某个数据元素,我直接通过结构体成员访问符'.'就可以了,为什么还要封装成一个函数?

因为在某些编译器中,就算我们访问数组越界了,它也不会报错,这样对我们检查程序中出现的错误是很不友好的,尤其是在一个项目中。像我们这样每个都封装成一个接口函数,然后每个函数内部都先进行asser断言判断是否出现了什么错误。这样对我们检查哪里出错来说是非常方便好用的。

相关推荐
想吃火锅100520 小时前
【leetcode】14.最长公共前缀js
算法·leetcode·职场和发展
云絮.21 小时前
数据库操作
数据库·mysql·算法·oracle
小林ixn1 天前
LeetCode 206. 反转链表(迭代 + 递归详解)
算法·leetcode·链表
凡人叶枫1 天前
Effective C++ 条款17:以独立语句将 newed 对象置入智能指针
java·linux·开发语言·c++·算法
菜鸟‍1 天前
LeetCode 1 27 和 704 || 两数之和 移除元素 二分查找
算法·leetcode·职场和发展
caimouse1 天前
Reactos 第1章 概述
c语言·开发语言·架构
退休倒计时1 天前
【每日一题】LeetCode 142. 环形链表 II TypeScript
算法·leetcode·链表·typescript
啊森要自信1 天前
【GUI自动化测试】控件、鼠标键盘操作与多场景自动化
c语言·开发语言·python·adb·ipython
popcorn_min1 天前
Digits 手写数字识别:随机森林多分类 + 像素级特征热力图
算法·随机森林·分类
liulilittle1 天前
拥塞控制:排水终止的两种决策:OR 与 AND
网络·tcp/ip·计算机网络·算法·信息与通信·tcp·通信