我们今天进入数据结构的学习,今天我们从梦开始的地方:顺序表。
什么是线性表
线性表是一种基本且广泛应用的数据结构,其主要特征如下:
- 有限序列:
线性表是由零个或多个数据元素构成的一个有限序列。这意味着线性表中的元素是有顺序的,并且这种排列是明确的,即每个元素都有一个确定的位置。- 相同类型:
线性表中的所有数据元素具有相同的数据类型。这确保了元素在计算机内存中占用相同大小的空间,便于管理和操作。- 一对一关系:
在线性表中,除了第一个元素(表头)可能没有前驱外,每个元素都有且仅有一个直接前驱;除了最后一个元素(表尾)可能没有后继外,每个元素也都有且仅有一个直接后继。这种一对一的关系形成了线性表的线性特性,即元素之间呈线性排列,不存在多对多或一对多的关系。- 线性表的长度:
线性表的长度是指其中包含的数据元素的个数,通常用n
表示。当n=0
时,称该线性表为空表。对于非空线性表,可以将其表示为(a1, a2, ..., an)
,其中ai
是第i
个数据元素。- 基本操作:
线性表支持一系列基本操作,包括但不限于:
- 初始化:创建一个新的线性表,分配必要的内存空间。
- 插入:在指定位置插入一个新元素,保持线性表的有序性。
- 删除:从指定位置移除一个元素,调整剩余元素以保持线性表的连续性。
- 查找:根据特定条件查找线性表中的元素,返回找到的元素或其位置信息。
- 修改:更改线性表中某个已知位置上的元素值。
- 遍历:按顺序访问线性表中的每一个元素,通常用于数据处理、计算或输出。
- 存储结构:
线性表可以通过不同的存储结构实现,主要分为两种:
- 顺序表:元素在计算机内存中按照它们在线性表中的逻辑顺序依次连续存放。访问元素可以根据其索引来直接定位,插入和删除操作则可能需要移动后续元素以维持连续性。
- 链表(如单向链表、双向链表、单向循环链表、双向循环链表):元素在内存中不一定连续存放,每个元素(结点)包含数据部分和一个或多个指针,用于链接前后相邻元素。链表通过指针关系维护元素间的线性关系,插入和删除操作通常只需修改指针,无需移动元素本身。
总结来说,线性表是一种数据元素呈线性排列、具有相同数据类型、支持高效基本操作的有限序列。它是许多复杂数据结构和算法的基础,广泛应用于各种计算机科学与工程问题中。
顺序表
顺序表是线性表的一种具体实现方式,其特点是将线性表中的元素在计算机内存中按照逻辑顺序(即它们在表中的位置顺序)依次、连续地存放。以下是顺序表的主要特点和操作细节:
特点:
- 内存连续性:
顺序表中的元素在内存中占据一片连续的存储区域。每个元素按照它们在线性表中的位置依次紧邻存放。这种内存布局使得可以通过元素的索引(位置序号)直接计算出其在内存中的地址。- 随机访问:
由于元素是连续存放的,给定一个元素的索引,可以立即通过简单的数学计算(通常是索引乘以单个元素的大小)得到其在内存中的地址,从而实现对任意元素的快速访问(O(1)时间复杂度)。这是顺序表相较于链式结构的一大优势。- 预分配空间:
创建顺序表时通常需要预先分配一段固定大小的内存空间来存放元素。随着元素的添加,若空间不足,可能需要动态地申请更大的内存区域,并将原有元素复制到新的内存区域中。这一过程称为“扩容”或“动态增长”,它可能导致额外的时间和空间开销。- 插入和删除的效率依赖于位置:
插入新元素或删除现有元素时,如果是在表尾进行操作,由于不需要移动其他元素,效率相对较高。但如果在表中间插入或删除元素,则需要将受影响的后续元素整体向后(插入时)或向前(删除时)移动一位以保持元素的连续性,这会导致操作的时间复杂度为 O(n),其中 n 是表中元素的数量。
基本操作:
- 初始化:分配一个足够容纳一定数量元素的连续内存区域,并设置初始元素数量为0。
- 插入:
- 表尾插入:检查当前元素数量是否达到预设的最大容量。如果没有,直接在末尾位置添加新元素,更新元素数量;如果已满,需要先进行扩容。
- 指定位置插入:首先确认插入位置的有效性(不能超出当前元素数量范围),然后将该位置及之后的所有元素依次向后移动一位,腾出空位后插入新元素,最后更新元素数量。
- 删除:
- 指定位置删除:确认删除位置的有效性,将要删除元素之后的所有元素依次向前移动一位以填补空缺,然后更新元素数量。
- 查找:
- 按索引查找:直接通过索引计算出元素在内存中的地址并访问,时间复杂度为 O(1)。
- 按值查找:需要从头至尾遍历整个顺序表,逐个比较元素值,找到匹配项或查找不到为止,时间复杂度为 O(n)。
- 修改:
- 给定元素的索引,可以直接通过索引计算出对应内存地址,然后更新该位置的元素值,时间复杂度为 O(1)。
- 遍历:
- 从表头开始,按照索引顺序逐个访问元素,直至表尾。
顺序表适用于元素数量稳定、需要频繁随机访问且对插入删除操作效率要求不高的场景。其优点在于简单易实现,支持高效的随机访问和尾部插入删除。缺点则是空间利用率可能不高(尤其当表未填满时),且在表中间进行插入删除操作时效率较低。在实际应用中,选择使用顺序表还是链表等其他线性表实现方式,通常取决于具体的应用需求和性能考量。
顺序表结构定义
我们接下来可以按照这些性质来实现顺序表,但是我们要首先要将顺序表的性质描述出来,定义出来:
我们用C语言实现一下:
#pragma once
#include<stdlib.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//定义顺序表存放的数据类型
typedef int SData;
//定义顺序表结构体
typedef struct SList
{
//动态数组
SData* _data;
//最大容量
size_t _capacity;
//当前数量
size_t _size;
}SList;
初始化顺序表
我们有了顺序表的结构,我们现在可以利用这个结构来完成顺序表的初始化:
//初始化顺序表
bool InitSList(SList* list,size_t capacity)
{
//开辟空间
list->_data = (SData*)malloc(sizeof (capacity) * capacity);
if( list->_data == NULL)
{
perror("malloc fail");
return false;
}
//设置当前数量
list->_size = 0;
return true;
}
我们可以来测试一下:
#include"Sqlist.h"
int main()
{
SList list;
bool flag = InitSList(&list,10);
if(flag == true)
{
printf("init success");
}
else
{
printf("init fail");
}
}
插入
表尾插入
表尾插入很简单,直接插入:
//尾插
bool TailInsert(SList* list,SData data)
{
//检查list是否为空
assert(list != NULL);
list->_data[list->_size++] = data;
return true;
}
但是这样有一个问题,如果线性表满了,这时候就不能插入了,要首先扩容:
扩容
扩容会用到realloc这个函数,这个函数可以在原来的地址上开辟指定空间:
//扩容
bool ReverseCpapcity(SList* list)
{
assert(list != NULL);
if(list->_size == list->_capacity) //此时当前数量等于最大容量时
{
//两倍扩容
SData* tmp = (SData*) realloc(list->_data,sizeof (SData) * 2 * list->_capacity);
if(tmp == NULL)
{
perror("realloc fail");
return false;
}
}
return true;
}
修改我们的尾插函数:
//尾插
bool TailInsert(SList* list,SData data)
{
//检查list是否为空
assert(list != NULL);
ReverseCpapcity(list);
list->_data[list->_size++] = data;
return true;
}
遍历
遍历的话我们直接打印就可以了:
//遍历
void PrintList(SList* list)
{
for(int i = 0; i < list->_size; i++)
{
printf("%d ",list->_data[i]);
}
}
我们可以测试一下:
#include"Sqlist.h"
int main()
{
SList list;
bool flag = InitSList(&list,10);
//尾插
TailInsert(&list,23);
TailInsert(&list,1);
TailInsert(&list,12);
TailInsert(&list,99);
//遍历
PrintList(&list);
return 0;
}
指定位置插入
现在我们想指定位置插入:
我们的从后往前搬运数据:
依次类推:
我们利用一个尾指针帮我们搬运数据:
//在任意位置插入
bool PosInsert(SList* list,size_t pos, SData data)
{
assert(list != NULL && pos >= 0);
ReverseCpapcity(list);
for(int end = list->_size - 1; end >= pos; end--)
{
list->_data[end + 1] = list->_data[end];
}
list->_data[pos] = data;
list->_size++;
return true;
}
#include"Sqlist.h"
int main()
{
SList list;
bool flag = InitSList(&list,10);
//尾插
TailInsert(&list,23);
TailInsert(&list,1);
TailInsert(&list,12);
TailInsert(&list,99);
//任意位置插入
PosInsert(&list,2,999);
//遍历
PrintList(&list);
return 0;
}
头插
头插可以复用在任意位置插入的代码:
//头插
bool FrontInsert(SList* list,SData data)
{
return PosInsert(list,0,data);
}
但这里注意一下这里:
这里的pos是size_t无符号整型,在头插的时候pos为0,end会减到-1,如果pos的类型是size_t,这里会有一个默认的整型提升,int类型的end会被提升成为size_t的类型,这个时候end就会变成正整数,会比pos大
为了解决这个问题,我们可以强转size_t pos为int:
//在任意位置插入
bool PosInsert(SList* list,size_t pos, SData data)
{
assert(list != NULL && pos >= 0);
ReverseCpapcity(list);
for(int end = list->_size;end >= (int)pos; --end)
{
list->_data[end + 1] = list->_data[end];
}
list->_data[pos] = data;
list->_size++;
return true;
}
#include"Sqlist.h"
int main()
{
SList list;
bool flag = InitSList(&list,10);
//尾插
TailInsert(&list,23);
TailInsert(&list,1);
TailInsert(&list,12);
TailInsert(&list,99);
//头插
FrontInsert(&list,233);
//遍历
PrintList(&list);
return 0;
}
删除
指定位置删除
//指定位置删除
bool PosErease(SList* list,size_t pos)
{
while(pos < list->_size)
{
list->_data[pos] = list->_data[pos + 1];
pos++;
}
list->_size--;
return true;
}
#include"Sqlist.h"
int main()
{
SList list;
bool flag = InitSList(&list,10);
//尾插
TailInsert(&list,23);
TailInsert(&list,1);
TailInsert(&list,12);
TailInsert(&list,99);
//头插
FrontInsert(&list,233);
//遍历
PrintList(&list);
//指定位置删除
PosErease(&list,3);
printf("\n");
//遍历
PrintList(&list);
return 0;
}
头删尾删
老样子我们可以复用:
//FrontErease
bool FrontErease(SList* list)
{
return PosErease(list,0);
}
//TailErease
bool TailErease(SList* list)
{
return PosErease(list,list->_size-1);
}
#include"Sqlist.h"
int main()
{
SList list;
bool flag = InitSList(&list,10);
//尾插
TailInsert(&list,23);
TailInsert(&list,1);
TailInsert(&list,12);
TailInsert(&list,99);
//头插
FrontInsert(&list,233);
//遍历
PrintList(&list);
//指定位置删除
PosErease(&list,3);
printf("\n");
//头删
FrontErease(&list);
PrintList(&list);
printf("\n");
//尾删
TailErease(&list);
PrintList(&list);
printf("\n");
return 0;
}
查找和修改
查找和修改都非常简单,这里我们来写写:
//找到指定元素
bool FindData(const SList* list,SData data)
{
for(int i = 0; i < list->_size; i++)
{
if(list->_data[i] == data)
{
return true;
}
}
return false;
}
//修改指定下标元素
bool FixList(SList *list,size_t pos,SData data)
{
assert(pos >= 0 && pos <= list->_size);
return list->_data[pos] = data;
}
#include"Sqlist.h"
int main()
{
SList list;
bool flag = InitSList(&list,10);
//尾插
TailInsert(&list,23);
TailInsert(&list,1);
TailInsert(&list,12);
TailInsert(&list,99);
//头插
FrontInsert(&list,233);
//遍历
PrintList(&list);
//指定位置删除
PosErease(&list,3);
printf("\n");
//头删
FrontErease(&list);
PrintList(&list);
printf("\n");
//尾删
TailErease(&list);
PrintList(&list);
printf("\n");
//找到指定元素
if(FindData(&list,23))
{
printf("true\n");
}
else
{
printf("false\n");
}
//修改
FixList(&list,0,456);
PrintList(&list);
printf("\n");
return 0;
}