文章目录
0.前言
🌵🌵
hello 大家好啊,为了练就编程的基本内功,今天回顾的是线性表中的顺序表。回顾顺序表之前,问自己几个问题。
- 物理结构与逻辑结构如何区分?
- 顺序表与数组有何联系?
话不多说,直接进入正文吧。
1.线性表
🐱 🐱
常见的线性表:顺序表,链表,栈,队列,字符串
线性表在逻辑上是线性结构,(连续的一条直线)
但是在物理结构不一定是连续的(比如链表)
数据结构的三要素是:逻辑结构、存储结构、数据运算。
逻辑结构
逻辑结构,指的就是数据之间的逻辑关系,从逻辑关系上来描述数据。逻辑结构又包括线性结构和非线性结构两种,线性表是一种典型的线性结构,图是一种典型的非线性结构。
那么,顺序表是逻辑结构吗?
不是的。虽然顺序表是一种线性结构,但是我们要注意,顺序表背后包含着顺序存储的意思。也就是说,顺序表既能够描述逻辑结构,也能够描述物理结构。所以,这是一种混合类型。
有序表是逻辑结构吗?
是的。有序表指的是数据元素按照一定顺序排列的线性表,除了描述两个元素之间有序的依赖关系以外,它再也没有别的意思了。
存储结构
存储结构包括顺序存储、链式存储、索引存储、散列存储(哈希存储)
-
顺序存储:所谓顺序存储,就是把逻辑上相邻的数据元素,存储到计算机的存储器上时,在物理上也是相邻的。最简单的实现就是数组。
-
链式存储:链式存储,就是我们所熟知的链表。我们无需像顺序存储那样,单独开辟一片连续的存储空间,只需要用到的时候直接分配空间,用指针来实现整个一对一逻辑结构的实现。
-
索引存储:这种存储方式类似于我们的书和目录的关系。比如书中第2章的内容在25页,我们想要找到它,只需要浏览目录,然后通过页码找到相关的内容即可。一般存储的时候都是**【关键字,地址】**这种形式。
-
散列存储:散列存储实际上就是做了一个函数关系的映射,由x去找y,如果y=x+1,那么x=1的元素就应该去y=2位置寻找。
2.顺序表
🐱 🐱
顺序表与数组
由于顺序表结构的底层实现借助的就是数组,因此对于初学者来说,可以把顺序表完全等价为数组,但实则不是这样。
数据结构是研究数据存储方式的一门学科,它囊括的都是各种存储结构,而数组只是各种编程语言中的基本数据类型,并不属于数据结构的范畴。
本质
顺序表:本质就是数组,动态增长,并且要求里面存储的数据必须是从左往右连续的
数组可以只存1 3 5位置,顺序表不行
数组是静态的利用malloc和realloc动态管理
[[13.动态内存管理#1 动态内存函数|动态管理]]
优点
- 按下标进行随机访问
- 空间地址连续,CPU高速缓存命中率比较高
CPU访问数据时,数据如果在缓存,那就命中
如果不在缓存,不命中
假设不命中时,一次性加载16字节
低命中是污染缓存的 --》正在执行的程序将不必要的数据从主存移到高速缓存,降低了数据处理的效率。–》 **《CSAPP》**中有详细介绍,感兴趣的可以去看看
缺陷
- 动态增容有性能消耗,通常伴随着空间浪费
- 头部或中间插入/删除数据,需要挪动数据,效率比较低
关于实现一个数据结构:
所谓实现一个数据结构,其实也就是管理数据结构里面的数据,也就是增删查改
3.顺序表的实现
🐱🐱
静态顺序表
首先我们来定义一下顺序表的结构
要求:
- 存储的数据从0开始,依次连续存储
- 数据以数组的形式存储
//静态顺序表(不推荐)
//存在问题,开小了,不够用;开大了,存在浪费问题
//一般来说,静态的数据结构都不太实用。
#define N 100
struct SeqList
{
int a[N];
int size;
};
动态顺序表
利用指针开辟一个动态的顺序表。
注意不要频繁的扩容,频繁的扩容消耗性能,利用capacity一次性扩较大的容量
typedef int SeqDataType;//利用typedef插入任意类型数据
typedef struct SeqList
{
SeqDataType* a;//指向动态开辟的数组
size_t size;//有效数据个数
size_t capacity;//容量
}SeqList;
此处也可以用柔性数组,但没必要,柔性数组也是需要malloc的。
[[13.动态内存管理#5 柔性数组|柔性数组]]
注意:当代码量大了之后,一定要写一部分,测一部分,而且不要先写菜单,写了菜单,调试起来就很麻烦。
SeqList.h
#pragma once //防止头文件重复包含
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<errno.h>
#include<string.h>
typedef int SeqDataType;//利用typedef插入任意类型数据
typedef struct SeqList
{
SeqDataType* a;//指向动态开辟的数组
size_t size;//有效数据个数
size_t capacity;//容量
}SeqList;
//初始化
void SeqListInit(SeqList* pseq);
//销毁
void SeqListDestory(SeqList* pseq);
//打印
void SeqListPrint(SeqList* pseq);
//扩容
void SeqCheckCapacity(SeqList* pseq);
//尾插
void SeqListPushBack(SeqList* pseq, SeqDataType x);
//头插
void SeqListPushFront(SeqList* pseq, SeqDataType x);
//尾删
void SeqListPopBack(SeqList* pseq);
//头删
void SeqListPopFront(SeqList* pseq);
//查找
int SeqListFind(SeqList* pseq,SeqDataType x);
//中间插入
void SeqListInsert(SeqList* pseq, size_t pos, SeqDataType x);
//中间擦除
void SeqListErase(SeqList* pseq, size_t pos);
//修改
void SeqListModify(SeqList* pseq, size_t pos, SeqDataType x);
其实,
void SeqListInit(SeqList& pseq);
传引用其实更好,不过此处是用c语言实现,就还是传指针了。[[1.入门#6 引用|传引用]]
Test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SeqList.h"
void TestSeqList()
{
SeqList s;//定义结构体变量
SeqListInit(&s);//必须传址
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushBack(&s, 5);
SeqListPushFront(&s, 0);
SeqListPushFront(&s, 0);
SeqListPushFront(&s, 0);
SeqListPrint(&s);
SeqListPopBack(&s);
SeqListPrint(&s);
SeqListPopBack(&s);
SeqListPrint(&s);
SeqListPopFront(&s);
SeqListPrint(&s);
SeqListPopFront(&s);
SeqListPrint(&s);
SeqListDestory(&s);
}
void TestSeqList2()
{
SeqList s;
SeqListInit(&s);
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushBack(&s, 5);
SeqListPrint(&s);
SeqListInsert(&s, 0, 30);
SeqListPrint(&s);
SeqListErase(&s, 0);
SeqListPrint(&s);
SeqListModify(&s, 0, -1);
SeqListPrint(&s);
SeqListDestory(&s);
}
int main()
{
TestSeqList2();
return 0;
}
SeqList.c
Tips:不要先写菜单,把接口都调正确了再写菜单
//静态顺序表(不推荐)
struct SeqList
{
int a[N];
int size;
};
初始化与销毁
注意:必须要传址调用,或者传引用。
[[1.入门#6 引用|传引用]]
//初始化
void SeqListInit(SeqList* pseq)
{
assert(pseq);
pseq->a = NULL;//pseq->对指针的解引用
pseq->capacity = 0;
pseq->size = 0;
}
//销毁
void SeqListDestory(SeqList* pseq)
{
assert(pseq);
free(pseq->a);
pseq->a = NULL;
pseq->capacity = pseq->size = 0;
}
扩容
realloc 扩容有原地扩和异地扩2种方式
[[13.动态内存管理#1 动态内存函数|realloc]]
void SeqCheckCapacity(SeqList* pseq)
{
//满了需要增容
if (pseq->size == pseq->capacity)
{
//一开始要赋予初始空间
int newcapacity = pseq->capacity == 0 ? 4 : pseq->capacity * 2;//通常增加2倍
SeqDataType* newA = realloc(pseq->a, sizeof(SeqDataType) * newcapacity);//第一次进来的时候,size=capacity=0,pseq->a=NULL, 此时realloc等价于malloc
//或者
if (newA == NULL)//判空
{
printf("realloc fail\n");
exit(-1);
}
else
{
pseq->a = newA;
pseq->capacity = newcapacity;
}
}
}
//判空也可以这么写,利用 errno 这个全局变量
if (newA == NULL)
{
printf("SeqListPushBack::%s\n", strerror(errno));
}
尾插头插
由于数组下标从0开始的特性,size的值恰好就算最后一个元素的下一个位置
插入时要注意是否有足够空间插。
注意 -> 优先级比 ++ 高
头插图解:
//尾插
void SeqListPushBack(SeqList* pseq, SeqDataType x)
{
assert(pseq);
SeqCheckCapacity(pseq);
pseq->a[pseq->size] = x;
pseq->size++;
}
//头插 数据需要往后挪,往后挪的过程中可能产生越界,因此先检查空间是否足够
void SeqListPushFront(SeqList* pseq, SeqDataType x)
{
assert(pseq);
SeqCheckCapacity(pseq);
int end = pseq->size - 1;//size的值恰好就是最后一个值的下一个
while (end >= 0)//把第一个位置的数据也挪走时就结束
{
pseq->a[end + 1] = pseq->a[end];
--end;
}
//结束时,end指向-1
pseq->a[0] = x;
pseq->size++;
}
//头插时间复杂度O(N)
//尾插时间复杂度O(1)
复用SeqListInsert的写法
//尾插
void SeqListPushBack(SeqList* pseq, SeqDataType x)
{
SeqListInsert(pseq, pseq->size, x);
}
//头插
void SeqListPushFront(SeqList* pseq, SeqDataType x)
{
SeqListInsert(pseq, 0, x);
}
打印
void SeqListPrint(SeqList* pseq)
{
assert(pseq);
for (int i = 0; i < pseq->size; i++)
{
printf("%d ", pseq->a[i]);
}
printf("\n");
}
尾删头删
尾删图解:
//尾删
void SeqListPopBack(SeqList* pseq)
{
assert(pseq);//防止传空指针
assert(pseq->size > 0);//用断言比较暴力,可以用if限制size<
--pseq->size;
}
//头删
void SeqListPopFront(SeqList* pseq)
{
assert(pseq);
assert(pseq->size > 0);//避免顺序表里都没元素了,还在删除
//必须从前往后挪动
int begin = 0;
//挪到begin == size-1就停止
while (begin < pseq->size - 1)
{
pseq->a[begin] = pseq->a[begin+1];
begin++;
}
--pseq->size;
}
//头删时间复杂度O(N)
//尾删时间复杂度O(1)
复用 SeqListPopBack 的写法
//尾删
void SeqListPopBack(SeqList* pseq)
{
SeqListErase(pseq, pseq->size - 1);
}
//头删
void SeqListPopFront(SeqList* pseq)
{
SeqListErase(pseq, 0);
}
一个小细节,为什么我这里都是用的前置–而不是后置呢?
一般来说:前置++ - - 比后置快一点。
查找
int SeqListFind(SeqList* pseq, SeqDataType x)
{
assert(pseq);
for (int i = 0; i < pseq->size; i++)
{
if (pseq->a[i] == x)
{
return i;
}
return -1;
}
}
中间插入/删除
插入图解:
当pos = size时,其实就是尾插。
当pos = 0时,其实就是头插。
//中间插入
void SeqListInsert(SeqList* pseq, size_t pos, SeqDataType x)
{
assert(pseq);
assert(pos <= pseq->size);//=size时表示的就是尾插
SeqCheckCapacity(pseq);
//从后往前挪动
size_t end = pseq->size - 1;
while (end >= pos)
{
pseq->a[end + 1] = pseq->a[end];
--end;
}
pseq->a[pos] = x;
pseq->size++;
}
也许有小伙伴会奇怪,为什么这里用的都是size_t 而不是int呢?
注意,我们模拟实现是要向标准看起的,C++STL库里的实现有关下标的表示都是size_t类型,毕竟下标都是 >=0 嘛。
注意,上面代码其实有一处bug,细心的你能否发现呢?
其实就是当第一次插入时,也就是size=0时,而我们让 PushBack 复用了 Insert ,也就是 pos 此时也是0,此时end = -1,由于end是无符号类型,因此会被转换成一个42亿多的大数,使用 >= 0,因此会陷入死循环。
那如何解决呢?
也许有人会说,那定义的时候把 end改成 int类型不就好了?
那我们来看看是不是这样就能解决呢?
//中间插入
void SeqListInsert(SeqList* pseq, size_t pos, SeqDataType x)
{
assert(pseq);
assert(pos <= pseq->size);//=size时表示的就是尾插
SeqCheckCapacity(pseq);
//从后往前挪动
int end = pseq->size - 1;
while (end >= pos)
{
pseq->a[end + 1] = pseq->a[end];
--end;
}
pseq->a[pos] = x;
pseq->size++;
}
调试查看一下:
震惊!!
明明end = -1 ,pos = 0 怎么还会进入循环呢?
不要慌,其实根因还是在于pos是 size_t 类型,int 类型的end 和 size_t 类型的 pos 进行比较时,会发生整形提升,将end 也转换成 size_t 类型,那能又死循环吗?
那难道 我们还必须将 pos 类型也定义成 int 才行吗?这可不行啊,这是标准规定好的,一定要按照标准来做。
而且如果改变了pos的类型,那么检查时就还得断言pos >= 0
assert(pos >= 0 && pos <= pseq->size);
一个简单的解决办法就是 比较的时候,将end pos都强转成int类型
这样第一次插入时,-1 >= 0 就不会进入while循环了,从而完成正常插入
//中间插入
void SeqListInsert(SeqList* pseq, size_t pos, SeqDataType x)
{
assert(pseq);
assert(pos <= pseq->size);//=size时表示的就是尾插
SeqCheckCapacity(pseq);
//从后往前挪动
size_t end = pseq->size - 1;
while ((int)end >= (int)pos)
{
pseq->a[end + 1] = pseq->a[end];
--end;
}
pseq->a[pos] = x;
pseq->size++;
}
当然这样解决看起来就不太符合正常的认知。
还有一种解决办法就是,让end指向的是size所在的位置,这样就只需要把前一个挪给后一个。
此时只需判断 end > pos
//中间插入
void SeqListInsert(SeqList* pseq, size_t pos, SeqDataType x)
{
assert(pseq);
assert(pos <= pseq->size);//=size时表示的就是尾插
SeqCheckCapacity(pseq);
//从后往前挪动
size_t end = pseq->size;
while (end > pos)
{
pseq->a[end] = pseq->a[end - 1];
--end;
}
pseq->a[pos] = x;
pseq->size++;
}
删除图解
结合上面的Insert的坑,我们已经知道该如何写啦。
插入删除时,要注意检查pos的合法位置。
删除时,需要从前往后挪。
//中间擦除
void SeqListErase(SeqList* pseq, size_t pos)
{
assert(pseq);
assert(pos < pseq->size);//不能=size,=size时没有元素的
size_t begin = pos + 1;
while (begin < pseq->size)
{
pseq->a[begin-1] = pseq->a[begin];
++begin;
}
pseq->size--;
}
关于检查力度
//温和的检查
if(pos > pseq->size)
{
printf("pos 越界:%d\n".pos);
return;
}
//暴力的检查
assert(pos <= pseq->size);
修改
void SeqListModify(SeqList* pseq, size_t pos, SeqDataType x)
{
assert(pseq);
assert(pos < pseq->size);
pseq->a[pos] = x;
}
4.尾声
🌵🌵
写文不易,如果有帮助烦请点个赞~ 👍👍👍
🌹🌹Thanks♪(・ω・)ノ🌹🌹
👀👀由于笔者水平有限,在今后的博文中难免会出现错误之处,本人非常希望您如果发现错误,恳请留言批评斧正,希望和大家一起学习,一起进步ヽ( ̄ω ̄( ̄ω ̄〃)ゝ,期待您的留言评论。
附GitHub仓库链接