数据结构:顺序表详解
- 一、 线性表
- 二、 顺序表概念及结构
- 1. 静态顺序表:使用定长数组存储元素。
- 2. 动态顺序表:使用动态开辟的数组存储。
- 三、接口实现
- 1. 创建
- 2. 初始化
- 3. 扩容
- 4. 打印
- 5. 销毁
- 6. 尾插
- 7. 尾删
- 8. 头插
- 9. 头删
- 10. 插入任意位置数据
- 11. 删除任意位置数据
- 12. 查找
- 13. 修改
- 四:所有代码
一、 线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的。
线性表在物理上存储时,通常以数组和链式结构的形式.
二、 顺序表概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
1. 静态顺序表:使用定长数组存储元素。
2. 动态顺序表:使用动态开辟的数组存储。
三、接口实现
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
基本增删查改接口
//对数据管理 --- 增删查改
void SLInit(SL* ps); //初始化
void SLDestory(SL* ps); //释放
void SLPrint(SL* ps); //打印
void SLCheakCapacity(SL* ps); //检查容量 -- 扩容
//头插头删 尾插尾删
void SLPushBack(SL* ps, SLDateType x); //尾插
void SLPopBack(SL* ps); //尾删
void SLPushFront(SL* ps, SLDateType x);//头插
void SLPopFront(SL* ps); //头删
//返回下标,没找到返回-1
int SLFind(SL* ps, SLDateType); //查找元素,返回下标
//在pos位置插入x
void SLInsert(SL* ps, int pos, SLDateType x); //任意位置插入
//在pos位置删除x
void SLErase(SL* ps, int pos); //任意位置删除
void SLModify(SL* ps, int pos, SLDateType x);//修改
1. 创建
由于在实际工程中,项目的实现都是采用模块化进行实现的。所以在此处博主也采用了模块化的方式进行实现。
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
//动态顺序表
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* a;//指向动态开辟的数组
int size; //有效数据的个数
int capacity;//容量空间的大小
}SL;
为了后续好修改类型数据,在此采用typedef将结构体类型struct SeqList 重新命名为SL。
在实际开发过程中,为了开发人员更好的输入数据,一般我们会将输入数据的数据类型重命名为SLDateType。(在本篇博客中,采用typedef将其数据类型int重命名为SLDateType)
2. 初始化
初始化时,理论上我们只需要开辟一个空间并置为空指针,并将结构体中的数据全部初始化为0即可。
但在实际开发过程中,我们一般会开辟一定大小的空间(本篇博客开4个空间,但具体开多少,各位可自行选择)。
代码实现:
void SLInit(SL* ps)
{
assert(ps);
ps->a = (SLDateType*)malloc(4 * sizeof(SLDateType));//开辟4个空间
if (ps->a == NULL)
{
perror("malloc");
exit(-1);
}
//开辟成功
ps->capacity = 4;//开辟多少空间,容量变为多少
ps->size = 0;
}
3. 扩容
在后续我们插入数据时,已开辟容量可能已经无法满足需求了。这是就需要扩容。
那一次扩到多少呢?
在实际开发过程中我们一般是扩到原有空间的两倍。(当然你也可以开1000倍,只要后台空间足够大)
代码实现:
void SLCheakCapacity(SL* ps)
{
assert(ps);
if (ps->size == ps->capacity)
{
//开辟空间X2
SLDateType* tmp = (SLDateType*)realloc(ps->a, ps->capacity * sizeof(SLDateType) * 2);
if (tmp == NULL)
{
perror("realloc");
exit(-1);
}
//开辟成功
ps->a = tmp;
ps->capacity *= 2;
}
}
4. 打印
上述函数定义完成后,我们通常需要测试打印以下相关数据,来判断相关函数定义是否成功.
代码实现:
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
5. 销毁
由于上述空间是动态开辟的。所以当我们使用完时,要及时销毁,释放空间。
代码实现:
void SLDestory(SL* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
6. 尾插
尾插:在尾部插入一个数据。
但是在数据的尾部插入一个数据时,我们需要考虑一个问题:原有空间是否可以容纳新的数据,是否需要扩容。
所以我们在插入数据时,要先调用 SLCheakCapacity函数来检查是否需要扩容。
代码实现:
void SLPushBack(SL* ps, SLDateType x)
{
assert(ps);
SLCheakCapacity(ps);//检查是否需要扩容
ps->a[ps->size] = x;
ps->size++;
}
7. 尾删
尾删:删除尾部最后的一个元素。
但尾删同样也要考虑一个问题,空间中是否还有数据给我们删除。
所以在进行尾删时,我们可以采用assert函数断言空间中还有数据。
代码实现:
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size >= 0);//断言空间中还有元素
ps->size--;//下标减1
}
在删除数据时,我们不用将原有数据删除。只需要下标减1即可。
原因在于我们时根据下标来使用数据的,当下标减1后,尾部最后一个数据便无法进行访问。
Tips:
- 越界是不一定报错的,系统对越界的检查是一种设岗抽查。
- 以VS2022为例,微软公司在数据的开始前和结尾后的一小段空间设有一些特殊值。当程序结束或内存空间释放时,编译器就会检查这些值是否发生改变, 从而触发程序的保护机制。但如果这些值没有发生改变,即使发生越界访问,程序也不会报错。就像如果你酒驾,交警只在二环设关卡,但只要你不去二环,你就没事不会被发现。(每个编译器略有差异)
8. 头插
头插:在数据最开始地方插入数据。
同样,头插也要调用 SLCheakCapacity函数来检查空间是否足够,是否需要扩容。
代码实现:
void SLPushFront(SL* ps, SLDateType x)
{
assert(ps);
SLCheakCapacity(ps);//检查是否需要扩容
//移动数据
int i = ps->size-1;
while (i >= 0)
{
ps->a[i + 1] = ps->a[i];
i--;
}
//移动数据完成,插入元素。同时有效个数加1
ps->a[0] = x;
ps->size++;
}
9. 头删
头删:删除数据最开始的元素。
思路和头插类似,只要下标从1开始,所有数据依次向前移动1位,再把有限个数减1即可。
同时头删也需要使用assert函数断言原有空间中还有数据可以删除。
代码实现:
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size >= 0);//空间中还有数据可以删除
//移动数据
for (int begin = 1; begin < ps->size; begin++)
{
ps->a[begin - 1] = ps->a[begin];
}
ps->size--;//有效个数减1
}
10. 插入任意位置数据
由于顺序表要求数据是连续存放的,所以我们只需要找到输入位置的下标pos即可。
【代码思路】:首先我们要检查输入下标是否合法,是有效下标;并检查是否有足够空间来容纳新数据,是否需要扩容。之后从输入的数据下标开始,所有元素向后移动一位,并把新数据插入到下标为pos处即可。
代码实现:
void SLInsert(SL* ps, int pos, SLDateType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);//检查下标是否合法
SLCheakCapacity(ps); //检查空间是否足够
//移动数据
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
11. 删除任意位置数据
【代码思路】:和插入任何位置数据思想类似。首先我们要检查输入下标pos是否合法。之后从输入下标开始,后一个元素拷贝到前一个元素空间。
代码实现:
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);//下标是否合法
//移动数据
int begin = pos+1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
12. 查找
【代码思路】:要查找某个元素。由于这里只是最简单的查找,我们直接暴力查找,遍历整个数组返回下标即可。更为复杂的数据查找,会有更高阶的数据结构来实现。
代码实现:
int SLFind(SL* ps, SLDateType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (x == ps->a[i])
return i;
}
return -1;
}
13. 修改
【代码思路】: 要实现修改数据,我们只需要先判断输入下标是否合法。在将对应下标数据进行修改即可。
代码实现:
void SLModify(SL* ps, int pos, SLDateType x)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
ps->a[pos] = x;
}
四:所有代码
SeqList.h源文件
#include <assert.h>
#include <stdlib.h>
//动态顺序表
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* a;
int size;
int capacity;
}SL;
//对数据管理 --- 增删查改
void SLInit(SL* ps);
void SLDestory(SL* ps);
void SLPrint(SL* ps);
void SLCheakCapacity(SL* ps);
//头插头删 尾插尾删
void SLPushBack(SL* ps, SLDateType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDateType x);
void SLPopFront(SL* ps);
//返回下标,没找到返回-1
int SLFind(SL* ps, SLDateType);
//在pos位置插入x
void SLInsert(SL* ps, int pos, SLDateType x);
//在pos位置删除x
void SLErase(SL* ps, int pos);
void SLModify(SL* ps, int pos, SLDateType x);
SeqList.c头文件
#define _CRT_SECURE_NO_WARNINGS
#include "SeqList.h"
void SLInit(SL* ps)
{
assert(ps);
ps->a = (SLDateType*)malloc(4 * sizeof(SLDateType));
if (ps->a == NULL)
{
perror("malloc");
exit(-1);
}
//开辟成功
ps->capacity = 4;
ps->size = 0;
}
void SLDestory(SL* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
void SLCheakCapacity(SL* ps)
{
assert(ps);
if (ps->size == ps->capacity)
{
SLDateType* tmp = (SLDateType*)realloc(ps->a, ps->capacity * sizeof(SLDateType) * 2);
if (tmp == NULL)
{
perror("realloc");
exit(-1);
}
ps->a = tmp;
ps->capacity *= 2;
}
}
void SLPushBack(SL* ps, SLDateType x)
{
assert(ps);
/*SLCheakCapacity(ps);
ps->a[ps->size] = x;
ps->size++;*/
SLInsert(ps, ps->size, x);
}
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size >= 0);
/*ps->size--;*/
SLErase(ps, 0);
}
void SLPushFront(SL* ps, SLDateType x)
{
assert(ps);
SLCheakCapacity(ps);
//移动数据
/*int i = ps->size-1;
while (i >= 0)
{
ps->a[i + 1] = ps->a[i];
i--;
}
ps->a[0] = x;
ps->size++;*/
SLInsert(ps, 0, x);
}
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size >= 0);
/*for (int begin = 1; begin < ps->size; begin++)
{
ps->a[begin - 1] = ps->a[begin];
}
ps->size--;*/
SLErase(ps, 0);
}
int SLFind(SL* ps, SLDateType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (x == ps->a[i])
return i;
}
return -1;
}
void SLInsert(SL* ps, int pos, SLDateType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheakCapacity(ps);
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
int begin = pos+1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
void SLModify(SL* ps, int pos, SLDateType x)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
ps->a[pos] = x;
}