数据结构——顺序表

前言

在介绍顺序表之前,我们首先要了解一下线性表线性表是n个具有相同特性的数据元素的有序队列 ,是数据结构中一种最基础、最常用的数据结构,其核心特征是:除第一个和最后一个元素外,每个元素有且仅有一个直接前驱和一个直接后继,元素之间呈现 “一对一” 的逻辑关系。常见的线性有:顺序表、链表、栈、队列……

线性表的逻辑结构一定是线性的,但物理结构(实际在计算机中的存储)不一定是连续的,线性表在物理上存储时,通常以顺序结构(数组)和链式结构的形式存储。(本章节主要介绍顺序存储,链式存储会在后期介绍)

1. 顺序存储结构(顺序表)

  • 核心原理:用一组连续的存储单元依次存储元素,逻辑顺序与物理顺序一致(如数组)。
  • 特点
    • 可通过下标直接访问任意元素(随机访问,时间复杂度O(1));
    • 插入 / 删除中间元素时需移动大量元素(时间复杂度O(n));
    • 需预分配内存,可能存在空间浪费或溢出。
  • 典型实现:C 语言数组、Python 的list、Java 的ArrayList

2. 链式存储结构(链表)

  • 核心原理:用非连续的存储单元存储元素,通过指针(或引用)将元素链接成序列。
  • 特点
    • 无法随机访问,需从头遍历(时间复杂度O(n));
    • 插入 / 删除元素只需修改指针(时间复杂度O(1),已知前驱位置时);
    • 空间按需分配,无浪费,但额外存储指针信息。
  • 典型实现:单链表、双链表、循环链表、Java 的LinkedList

一. 顺序表

1.1 顺序表的概念

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

既然顺序表是通过数组存储的,那么顺序表和数组有什么区别?

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

我们可以通过下面的图画对数组与顺序表的区别有更深刻的理解

1.2 顺序表的特征

  1. 存储连续性
    所有元素在内存中占用连续的存储空间,例如数组就是典型的顺序表实现。假设第一个元素地址为Loc(e₁),每个元素占用k个字节,则第i个元素的地址为:
    Loc(eᵢ) = Loc(e₁) + (i-1)×k
    这使得顺序表可以通过下标直接访问任意元素(时间复杂度O(1))。

  2. 元素同类型
    所有元素必须是相同数据类型,保证每个元素占用的存储空间大小一致,才能通过上述公式计算地址。

  3. 大小固定或动态扩展

    • 静态顺序表:使用固定大小的数组实现,容量在初始化时确定,无法动态调整(可能溢出)。
    • 动态顺序表:当存储空间不足时,会重新申请一块更大的连续内存,将原有元素复制过去 。

1.3. 顺序表分类

1.3.1 静态顺序表

1.3.1.1 静态顺序表的概念

概念:使用定长的数组存储数据

缺陷:空间给定后不改变,如果空间给小了,可能会导致溢出,给多了又会造成空间的浪费

1.3.1.2 动态顺序表的实现
//静态顺序表
#define N 7 //给定数组的大小
typedef int SLDataType;
typedef struct SeqList
{
	SLDataType arr[N]; // 定长数组
    int size;//有效数据个数
}SL;

1.3.2 动态顺序表

1.3.2.1 动态顺序表的概念

用连续内存存储元素,同时解决了静态顺序表(固定大小数组)的空间限制问题,能够根据元素数量动态调整存储空间大小。

1.3.2.2 动态顺序表的实现

动态顺序表的实现包括增删改查等各个方面,接下来我将会结合代码和注释对线性表进行深入讲解

SeqList.h 文件  定义结构并声明函数

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

typedef int SLDatatype;
typedef struct SeqList
{
	SLDatatype* arr;//
	int size;//有效数据个数
	int capacity;//容量大小
}SL;

//初始化
void SLInit(SL* ps);

//销毁
void SLDestory(SL* ps);

//打印
void SLPrint(SL* ps);

//扩容
SLCheckCapacity(SL* ps);

//尾插
void SLPushBack(SL* ps, SLDatatype x);

//头插
void SLPushFront(SL* ps, SLDatatype x);

//尾删
void SLPopBack(SL* ps);

//头删
void SLPopFront(SL* ps);

//查找
int SLFind(SL* ps, SLDatatype x);

//在指定位置前插入数据
void SLInsert(SL* ps, int pos,SLDatatype x);

//在指定位置之后插入数据
void SLInsertAfter(SL* ps, int pos, SLDatatype x);

//删除指定位置数据
void SLErase(SL* ps, int pos);

//修改指定位置数据
void SLModify(SL* ps, int pos,SLDatatype x);

SeqList.c 文件   定义函数(将 .h 中的函数进行实现)

#include"SeqList.h"

//初始化
//初始化是要采用传址调用,而不能采用传值调用
//因为传值调用传的是sl的地址,函数内部通过修改指针可以直接修改原始变量
void SLInit(SL* ps)
{
	assert(ps);
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}
//err示例,传值调用
//因为传值调用,函数接收的是sl变量的一份临时拷贝(副本)
//在SLInit函数中修改的是形参ps的成员,而非test01()中定义的结构体变量sl
//函数执行完毕后,ps会被销毁,而sl的成员值仍未被初始化
//void SLInit(SL ps)
//{
//	ps.arr = NULL;
//	ps.size = ps.capacity = 0;
//}

//打印
void SLPrint(SL* ps)
{
	assert(ps);
	int i = 0;
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("\n");
}

//销毁
void SLDestory(SL* ps)
{
	//申请的空间要释放掉
	free(ps->arr);
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

//扩容
SLCheckCapacity(SL* ps)
{
	//判断空间是否充足
	if (ps->size == ps->capacity)
	{
		//扩容
		int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		SLDatatype* tmp = realloc(ps->arr, newcapacity * sizeof(SLDatatype));
		if (tmp == NULL)
		{
			perror("malloc fail!");
			exit(1);
		}
		ps->arr = tmp;
		ps->capacity = newcapacity;
	}
}

//尾插
void SLPushBack(SL* ps, SLDatatype x)
{
	SLCheckCapacity(ps);
	//空间充足
	ps->arr[ps->size] = x;
	ps->size++;//插入后,有效数据个数+1,size向后移动一位
}

//头插
void SLPushFront(SL* ps, SLDatatype x)
{
	//判断空间是否充足
	SLCheckCapacity(ps);
	//数据整体向后挪动一次
	for (int i=ps->size;i>0;i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[0] = x;
	ps->size++;//插入后,有效数据个数+1,size向后移动一位
}

//尾删
void SLPopBack(SL* ps)
{
	assert(ps && ps->size);
	//ps和ps->size都不能为空,因为不能传空指针,所以ps不能为空
	//数据表不能为空才能删除数据,所以size不能为空
	ps->size--;
	//不能free,因为free是释放一块连续的空间,如果free,整个数组都会被释放
	//数据表中有多少个有效数据取决于size,而不是实际有几个数,所以size-1就可以了
	//size向前移动一位,尾部的那个数据虽然仍然存在,但是我们就可以把它当成是一个随机值,比如,当我们调试的时候,除了我们插入的数据,其他位置都是随机值
}

//头删
void SLPopFront(SL* ps)
{
	assert(ps && ps->size);
	for (int i = 0; i < ps->size-1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}

//查找
//在顺序表中查找该数据,并返回下标
//这样如果想要在指定的数据后插入数据时,就不用自己去找这个数据所处的下标,而是调用该函数即可
int SLFind(SL* ps, SLDatatype x)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
		{
			//找到了
			return i;
		}
	}
	//未找到
	return -1;
}

//在指定位置前插入数据
void SLInsert(SL* ps, int pos, SLDatatype x)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	//空间不足
	SLCheckCapacity(ps);
	//空间足够
	//pos及以后的数据向后移动一位
	for (int i = ps->size; i > pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos] = x;
	ps->size++;
}

//在指定位置之后插入数据
void SLInsertAfter(SL* ps, int pos, SLDatatype x)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	//空间不足
	SLCheckCapacity(ps);
	for (int i = ps->size; i-1 > pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos + 1] = x;
	ps->size++;
}

//删除指定位置数据
void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	for (int i = pos; i < ps->size-1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}

//修改指定位置数据
void SLModify(SL* ps, int pos, SLDatatype x)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	ps->arr[pos] = x;
}

:大家可自行创建测试文件去测试并实现一下以上代码)

总结:如果大家能够理解并独自实现以上的代码,那么相信大家对顺序表一定能够掌握的非常好。后续我也会结合leetcode上的题目对顺序表的应用进行更深刻的讲解

如有不足或改进之处,欢迎大家在评论区积极讨论,如果文章对你有帮助,就点赞收藏关注支持一下作者吧,你的支持就是我的动力!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值