顺序表(1)

简介: 顺序表(1)

今天介绍数据结构中的顺序表。

顺序表和链表都是最基础和最常见的实用的数据结构。

线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。

线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...

线性表在逻辑上是线性结构,也就说是连续的一条直线。(也就是说在逻辑上是依次存储的)但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组链式结构的形式存储。

顺序表Sequential List

顺序表是用一段 物理地址连续的存储单元 依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。

顺序表简单来说就是一个数组,要求这个数组的空间是连续的,且存储的数据也必须是从头开始连续存储。

静态顺序表

静态顺序表:使用定长数组存储元素。

静态顺序表的长度是固定的。 虽然在不少的书籍和课本上都喜欢实用静态顺序表去讲解和测试。但是静态顺序表的空间是固定的,空间给大了,很浪费;空间给小了,不够实用。所以在我们后面的学习中,我们需要去学习动态的顺序表。它的实用性实践性更高,当然更加难。


动态顺序表

动态顺序表:使用动态开辟的数组存储。

动态顺序表虽然可以规避一些空间浪费,但是还是会有一点点浪费。

实用动态内存开辟函数去开辟空间的时候,每次空间不够的时候都会扩容,每次扩容都会付出一定程度上的代价。所以每次扩容可以稍微多扩容一点点,减少扩容的频率。扩容是根据需求扩容,那我们最常用的扩容倍数就是:2倍 1.5倍。但是不是一定要求是这些倍数。2倍数是一个相对合适的量,扩容的频率会越来越低,具体情况具体分析!


主函数Test.c

静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空

间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间

大小,所以下面我们实现动态顺序表。本章实现顺序表的头插尾插头删尾删等功能进行实现。

int main()
{
  SL ps;//创建一个结构题变量-传地址调用🆗-形参是实参的一份临时拷贝
  test1(&ps);//测试尾插 
  test2(&ps);//测试头插
  test3(&ps);//测试尾删
  test4(&ps);//测试头删
  return 0;
}

test1

#include"SeqList.h"
void test1(SL*ps)//测试尾插
{
  SLInit(ps);
  SLPushBack(ps, 10);
  SLPushBack(ps, 20);
  SLPushBack(ps, 30);
  SLPushBack(ps, 40);
  SLPrint(ps);
  SLDestroy(ps);
}

test2

void test2(SL*ps)//测试头插
{
  SLInit(ps);
  SLPushFront(ps, 10);
  SLPushFront(ps, 20);
  SLPushFront(ps, 30);
  SLPushFront(ps, 40);
  SLPrint(ps);
  SLDestroy(ps);
}

test3

void test3(SL*ps)//测试头删
{
  SLInit(ps);
  SLPushBack(ps, 10);
  SLPushBack(ps, 20);
  SLPushBack(ps, 30);
  SLPushBack(ps, 40);
  SLPopBack(ps);
  SLPopBack(ps);
  SLPrint(ps);
  SLDestroy(ps);
}

test4

void test4(SL*ps)//测试尾删
{
  SLInit(ps);
  SLPushBack(ps, 10);
  SLPushBack(ps, 20);
  SLPushBack(ps, 30);
  SLPushBack(ps, 40);
  SLPopFront(ps);
  SLPopFront(ps);
  SLPrint(ps);
  SLDestroy(ps);
}

头文件&函数声明SeqList.h

头文件

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>//断言

函数声明

  • SeqList声明
//声明一个结构体
typedef int SLDataType;
typedef struct SeqList
{
  SLDataType* a;//如果后期a的类型改变就很方便
  int size;//有效数据
  int capacity;//空间容量
}SL;//SL是这个结构体的类型,用typedef定义更加方便了
  • SeqList初始化
1. //初始化
2. void SLInit(SL* ps);
  • SeqList释放&销毁
1. //释放销毁
2. void SLDestroy(SL* ps);
  • SeqList展示
1. //展示
2. void SLPrint(SL* ps);
  • 尾插 头插 尾删 头删
void SLPushBack(SL* ps, SLDataType x);//尾插
void SLPushFront(SL* ps, SLDataType x);//头插
void SLPopBack(SL* ps);//尾删
void SLPopBack(SL* ps);//头删

函数实现SeqList.c

初始化SLInit

#include"SeqList.h"
//初始化
void SLInit(SL* ps)
{
  ps->a = NULL;
  ps->size = 0;
  ps->capacity = 0;
}
//关于初始化 可以首先置为NULL  也可以首先放点值  
// memset一般用于数组初始化 直接初始化更加清晰

释放销毁SLDestroy

//销毁
void SLDestroy(SL* ps)
{
  if (ps->a != NULL)
  {
    free(ps->a);
    ps->a = NULL;
    ps->size = 0;
    ps->capacity = 0;
  }
}

扩容SLCheckCapacity

这个函数是我们自己封装,可以不用去函数声明,秘密。但是记住一定要在使用这个CheckCapacity 之前我们一定要声明或者放在使用之前。


扩容就要用到我们在【动态内存管理】讲解的realloc函数了,扩容分为原地扩容和异地扩容,忘记的小伙伴再复习一下哦,一定要熟悉【戳一戳】:C语言之动态内存管理篇(1)-CSDN博客


原地扩容和异地扩容判断和最优写法

realloc函数存储空间起始位置是可以传NULL空指针的

realloc函数所需要开辟的空间一定要包含旧空间的大小

异地扩容对之前的空间不需要手动free,而是操作系统已经帮助释放了原来旧的空间

//扩容
void SLCheckCapacity(SL* ps)
{
  if (ps->size == ps->capacity)//容量满了需要扩容的条件
  {
    int newcapacity = ps->capacity == 0 ? 4 : 2 * (ps->capacity);
    SLDataType* tmp = (SLDataType*)raelloc(ps->a, sizeof(SLDataType) * newcapacity);
    if (tmp == NULL)
    {
      perror("CheckCapacity");//
      return;
    }
    ps->a = tmp;
    ps->capacity = newcapacity;
  }
}

打印SLPrint

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

尾插SLPushBack

  • 首先在size位置放值x
  • size往后移动++


//尾插
void SLPushBack(SL* ps, SLDataType x)
{
  SLCheckCapacity(ps);//扩容
  ps->a[ps->size] = x;
  ps->size++;
}

头插SLPushFront

在顺序表的前面插入数据,是没有向前扩容和插入空间这种做法的。这段空间整体是连续的,地址是连续的,又要求数据是连续的。所以我们唯一可用的方法就是把数据往后挪动。当然我们可以用函数memcpy和memove来实现,这里我们就手动来写。如果不想挪动数据,只有一种方法就是使用链表。


  • 首先把size-1的位置移动到size的位置,整体挪动完成。
  • 再在第一个位置放数值


void SLPushFront(SL* ps, SLDataType x)
{
  SLCheckCapacity(ps);
  int end = ps->size - 1;
  while (end >= 0)//注意可以等于0 size为1 但是不能为负数会越界访问
  {
    ps->a[end+1] = ps->a[end];
    end--;
  }
  ps->a[0] = x;
  ps->size++;
}

还想补充到的是:尽量别频繁的使用头插法,大量的使用不太行。

尾删SLPopBack

尾删也是非常简单的,有人可能会说把要删除的值赋值为-1或者NULL。但是我们并不能确定顺序表元素的类型,万一类型改变,这里再去改变就不方便了。而且万一这个值本来就是-1。所以直接size--即可。后期插入等操作会把这个值给覆盖掉。


  • size-- 意味着着只有--之后的数值有效。意味着我们头尾插会把无效数据覆盖。
  • 无效数据不需要free
  1. 第一动态内存是不支持分期free。
  2. 第二后续操作会覆盖无效数据,空间可以重复利用。
  3. 第三单独free,顺序表的空间就不连续了

//尾删
void SLPopBack(SL* ps)
{
  assert(ps->size);//本质问题就是害怕这个顺序表空了还在删除
  ps->size--;
}

规避过度头删


头删SLPopFront

  • 头插是从后向前挪动数据覆盖
  • 头删是从前向后挪动数据覆盖
  • size--即可

//头删
void SLPopFront(SL* ps)
{
  assert(ps->size);
  int begin = 1;
  while (begin < ps->size)
  {
    ps->a[begin] = ps->a[begin + 1];
    begin++;
  }
  ps->size--;
}

✔✔✔✔✔最后,感谢大家的阅读,若有错误和不足,欢迎指正。下章继续顺序表。

  • 结构体传址调用,形式参数是实际参数的一份临时拷贝
  • size永远指向所存储元素的下一个位置,顺序表是数组,下标从0开始。
  • realloc函数的注意点
  • assert的点

代码---------→【唐棣棣 (TSQXG) - Gitee.com

联系---------→【邮箱:2784139418@qq.com】

目录
相关文章
|
7月前
|
存储 测试技术 C语言
顺序表详解(SeqList)
顺序表详解(SeqList)
217 0
|
存储
【顺序表】
【顺序表】
53 0
|
6月前
|
算法
顺序表的应用
顺序表的应用
44 5
|
6月前
|
存储 算法
顺序表专题
顺序表专题
46 4
|
6月前
|
存储
2.顺序表_链表(附练习)
2.顺序表_链表(附练习)
|
6月前
|
存储
25.顺序表专题
25.顺序表专题
|
7月前
|
存储 缓存
【顺序表和链表的对比】
【顺序表和链表的对比】
|
7月前
|
存储
顺序表讲解
顺序表讲解
58 0
|
7月前
顺序表的实现
顺序表的实现
|
测试技术
顺序表(2)
顺序表(2)
554 0