练就基本内功之搞懂顺序表【数据结构】【C语言实现】

请添加图片描述

0.前言

🌵🌵

hello 大家好啊,为了练就编程的基本内功,今天回顾的是线性表中的顺序表。回顾顺序表之前,问自己几个问题。

  1. 物理结构与逻辑结构如何区分?
  2. 顺序表与数组有何联系?

话不多说,直接进入正文吧。

1.线性表

🐱 🐱

常见的线性表:顺序表,链表,栈,队列,字符串
线性表在逻辑上是线性结构,(连续的一条直线)
但是在物理结构不一定是连续的(比如链表)

数据结构的三要素是:逻辑结构、存储结构、数据运算

逻辑结构

逻辑结构,指的就是数据之间的逻辑关系,从逻辑关系上来描述数据。逻辑结构又包括线性结构和非线性结构两种,线性表是一种典型的线性结构,图是一种典型的非线性结构。

那么,顺序表是逻辑结构吗?

不是的。虽然顺序表是一种线性结构,但是我们要注意,顺序表背后包含着顺序存储的意思。也就是说,顺序表既能够描述逻辑结构,也能够描述物理结构。所以,这是一种混合类型

有序表是逻辑结构吗

是的。有序表指的是数据元素按照一定顺序排列的线性表,除了描述两个元素之间有序的依赖关系以外,它再也没有别的意思了。

存储结构

存储结构包括顺序存储、链式存储、索引存储、散列存储(哈希存储)

  1. 顺序存储:所谓顺序存储,就是把逻辑上相邻的数据元素,存储到计算机的存储器上时,在物理上也是相邻的。最简单的实现就是数组。

  2. 链式存储:链式存储,就是我们所熟知的链表。我们无需像顺序存储那样,单独开辟一片连续的存储空间,只需要用到的时候直接分配空间,用指针来实现整个一对一逻辑结构的实现。

  3. 索引存储:这种存储方式类似于我们的书和目录的关系。比如书中第2章的内容在25页,我们想要找到它,只需要浏览目录,然后通过页码找到相关的内容即可。一般存储的时候都是**【关键字,地址】**这种形式。

  4. 散列存储:散列存储实际上就是做了一个函数关系的映射,由x去找y,如果y=x+1,那么x=1的元素就应该去y=2位置寻找。

2.顺序表

🐱 🐱

顺序表与数组

由于顺序表结构的底层实现借助的就是数组,因此对于初学者来说,可以把顺序表完全等价为数组,但实则不是这样。
数据结构是研究数据存储方式的一门学科,它囊括的都是各种存储结构,而数组只是各种编程语言中的基本数据类型,并不属于数据结构的范畴。

本质

顺序表:本质就是数组,动态增长,并且要求里面存储的数据必须是从左往右连续的
数组可以只存1 3 5位置,顺序表不行
数组是静态的

利用malloc和realloc动态管理
[[13.动态内存管理#1 动态内存函数|动态管理]]

优点

  1. 按下标进行随机访问
  2. 空间地址连续,CPU高速缓存命中率比较高

CPU访问数据时,数据如果在缓存,那就命中
如果不在缓存,不命中
假设不命中时,一次性加载16字节
低命中是污染缓存的 --》正在执行的程序将不必要的数据从主存移到高速缓存,降低了数据处理的效率。

–》 **《CSAPP》**中有详细介绍,感兴趣的可以去看看

缺陷

  1. 动态增容有性能消耗,通常伴随着空间浪费
  2. 头部或中间插入/删除数据,需要挪动数据,效率比较低

关于实现一个数据结构

所谓实现一个数据结构,其实也就是管理数据结构里面的数据,也就是增删查改

3.顺序表的实现

🐱🐱

静态顺序表

首先我们来定义一下顺序表的结构
要求:

  1. 存储的数据从0开始,依次连续存储
  2. 数据以数组的形式存储
//静态顺序表(不推荐)
//存在问题,开小了,不够用;开大了,存在浪费问题
//一般来说,静态的数据结构都不太实用。
#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的值恰好就算最后一个元素的下一个位置

插入时要注意是否有足够空间插。

注意 -> 优先级比 ++ 高

头插图解:

image-20220310220906245

//尾插
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");
}

尾删头删

尾删图解:

image-20220310221809572

//尾删
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;
	}
}

中间插入/删除

插入图解:

image-20220311102515753

当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,因此会陷入死循环。

image-20220311104833553

那如何解决呢?

也许有人会说,那定义的时候把 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++;
}

调试查看一下:

image-20220311105501218

震惊!!

明明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的坑,我们已经知道该如何写啦。

image-20220311122426021

插入删除时,要注意检查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仓库链接

请添加图片描述

  • 15
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值