C语言之“动态顺序表的增删改查及拥有附加功能的通讯录实现”

简介: C语言之“动态顺序表的增删改查及拥有附加功能的通讯录实现”

顺序表

      顺序表是线性表的一种,而线性表指的是具有相同特性的一类数据结构的统称,这些相同特性即在逻辑结构(人为想象)上一定是线性的,在物理结构(内存存储中)上不一定是线性的

       顺序表的底层结构是数组(在后续的顺序表实现中很重要),所以顺序表在逻辑结构上是线性的,在物理结构上也是线性的

静态顺序表

typedef struct 静态顺序表名
{
   ElementType data[MaxSize]; //用于存储元素的数组
   int size;                  //当前顺序表的元素个数
} ;

缺点:空间小不够用,空间大太浪费

动态顺序表

typedef struct 动态顺序表名
{
   ElementType *data; //存储元素的指针
   int size;          //当前顺序表的元素个数
   int capacity;      //当前顺序表的容量
} ;

!!! 关于动态顺序表你必须要知道的内容:

当创建动态顺序表时,会动态分配一块内存空间,并将 data 指针指向这个内存块。这个内存块可以根据需要进行动态调整,以适应顺序表的容量需求:

动态顺序表实现:

我们这里先将函数的主体逻辑进行展示,便于对后续的函数实现有一个基本的认知:

test.c文件:

#include "SeqList.h"
void SLTest()
{
  SL sl;
  SLInit(&sl);  //对动态顺序表的初始化
  //顺序表尾插
  SLPushBcak(&sl, 1);
  SLPushBcak(&sl, 2);
  SLPushBcak(&sl, 3);
  SLPushBcak(&sl, 4);//1 2 3 4
  SLPrint(&sl);
  //顺序表头插
  SLPushFront(&sl, 5);//5 1 2 3 4
  SLPushFront(&sl, 6);//6 5 1 2 3 4 
  SLPushFront(&sl, 7);//7 6 5 1 2 3 4
  SLPrint(&sl);
  //尾删
  SLPopBack(&sl);
  SLPrint(&sl);
  SLPopBack(&sl);
  SLPrint(&sl);
  //在指定位置前插入数据
  SLInsert(&sl, 1, 8);
  SLPrint(&sl);
  //删除指定位置的数据
  SLErase(&sl, 2);
  bool findRet = SLFind(&sl, 8);
  if (findRet) {
    printf("找到了!\n");
    SLPrint(&sl);
  }
  else
  {
    printf("没找到!\n");
  }
  SLDestroy(&sl);
}
int main()
{
  SLTest();
  return 0;
}

①关于SL sl的解释:SL是一个重命名后的结构体类型名,SL sl表示我们创建了一共SL结构体类型的变量sl,当然你也可以叫它struct SeqList结构体类型的变量sl,只不过后者读起来麻烦。

②关于&sl的解释:当你声明一个结构体变量 sl 时,sl 的类型是 struct SeqList,它是这个结构体类型的一个实例,因此,sl 是整个结构体的变量,sl 的地址就是整个结构体 struct SeqList 的地址,通过sl->的方式你可以访问结构体中的每个成员变量。

顺序表的初始化

#pragma once
#include <stdio.h>
//动态顺序表
typedef int SLDataType;  //切换类型时只需要切换int即可
struct SeqList
{
  SLDataType* a;
  int size;     
  int capacity;  
};  
typedef struct SeqList SL;
//顺序表的初始化
void SLInit(SL* ps);

①关于“typedef int SLDataType”的解释:我们将int类型的重命名为SLDataType,后者有着与一样的大小等,增强了代码的可读性和可维护性:比如我们想要切换指针a的类型时只需要将int改为该类型

②关于“SLDataType* a”的解释:在顺序表中,底层逻辑是使用数组来存储数据。成员变量 a 可以被看作是数组的首元素地址,通过指针 a 可以访问数组中的元素。(理解这一点很重要)

void SLInit(SL* ps) {
  ps->a = NULL;         //ps->a表示指向结构体SL中的成员变量的指针
  ps->size = ps->capacity = 0;  
}

①关于SL* ps的解释:在前面的test.c文件中我们知道了SLInit函数的实参为&sl,即获取了整个结构体的地址,这里的形参用SL* ps来接收它,ps此时就是一个指向SL结构体类型的指针,我们可以通过它来访问SL结构体中的各个成员变量

②关于ps->a = NULL的解释:一般情况下我们使用指针访问结构体类型变量的时候都是使用->的方式,这里代表的意思是访问SL结构体中的a成员变量,然后又由于成员a的结构体变量为SLdataType*类型(实际是int*)的,所以在未使用指针变量a之前要先将它设置为空指针。

③关于ps->size = ps ->capacity = 0的解释:这里就是讲SL结构体中的成员变量size和capacity赋值为0,目的就是进行初始化

顺序表的释放

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

①关于if(ps->a)、free(ps->a)以及ps->a -NULL的解释:其实也没啥好解释的就是判断使用完a指针后a指针指向的内存空间是否为空(或者a是否为空指针),如果不为空就释放内存空间,释放完成后就像a指针设置为空指针就行

之后的所有函数开头的非空判断不做解释,自己理解

顺序表的尾插

//尾插
void SLPushBcak(SL* ps, SLDataType x) //传递结构体指针
{
  if (ps == NULL && SLIsEmpty(ps) != NULL)
  {
    return;
  }
  //1、空间不够,扩容(一般以2倍或者1.5倍进行扩容)
  SLCheckCapacity(ps);
  //2、空间足够,直接尾插
  ps->a[ps->size++] = x;  //直接把它当成数组更容易理解
}

①关于(SL* ps,SLDataType x)的解释:ps不用解释它是指向整个结构体,SLDataType x表示的是我们要操作的数据。

②关于SLCheckCapacity(ps)的解释:该函数是用来处理空间不够的问题的其代码如下:

void SLCheckCapacity(SL* ps)
{
  if (ps->size == ps->capacity)//如果当两个值相等时,证明再要进行插入操作就需要进行开辟新的位置
  {
    int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
    SLDataType* tmp = (SLDataType*)realloc(ps->a, newCapacity * 2 * sizeof(SLDataType));
    if (tmp == NULL)//防止扩容失败
    {
      perror("realloc fail\n");
      return 1;
    }
    ps->a = tmp;
    ps->capacity = newCapacity;
  }
}

1、关于if (ps->size == ps->capacity)的解释:我们知道顺序表的底层逻辑是数组,我们现在的尾插函数其实就是在数组的最后一个位置后面插入一个新的元素,这个数组的空间我们之前是以及开辟过的,但是当我们使用的时候可能不会记住后面到底还有多少空间没有被使用,申请的空间是否以及被填满了?它的判断条件就是顺序表中有效的数据个数size与顺序表当前所占空间大小capacity之间的关系,当二者相等时证明空间刚好被用完,此时想要在顺序表后面重新插入数据就需要在顺序表后面开辟新的内存空间

2、关于int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity的解释:这里是一个三目运算符,意思是如果此时capacity的值为零即顺序表现在所占空间为0那么就申请四个整型的内存空间,如果不为零那就将capacity的值扩大两倍即新内存空间是旧内存空间的两倍(注意这里只是表达了扩大两倍的想法还并未进行真正的扩容),判断完成的值会被赋值给newCapacity(其实这里的newCapacity也是在程序报错后才发现需要进行补充的,如果没有这一步会导致在下面真正开辟的时候由于顺序表初始化中ps->capacity=0的存在capacity的值为0最后开辟的内存空间就会为0了,创建一个newCapacity先进行简单的验证一下代码会更加的严谨)

3、关于 (SLDataType*)realloc(ps->a, newCapacity * 2 * sizeof(SLDataType))的解释:在上面我们将想要进行扩容多少内存空间的想法传递给了newCapacity,此时我们就要开始真正的内存开辟,前面我们学过了三种动态内存开辟的函数malloc、calloc以及realloc,在这里我们选择realloc函数因为它能使动态内存管理更加灵活,它的函数原型是void* realloc(void* ptr,size_t size);在这里的意思就是为结构体中的a成员变量开辟新的内存空间,调整后的空间大小为newCapacity*2*sizeof(SLDataType),后面乘上一个sizeof(SLDataType)的这是为了使开辟的新空间能让SLDataType*a类型数据都能够刚好放下(这样解释应该能理解吧~,如果你懂可以不用看关于这点的解释)

4、关于SLDataType* tmp 的解释:我们不能保证每次都成功开辟动态内存空间,所以我们采用一个临时的指针变量tmp指向我们动态开辟的内存空间的结果,如果我们开辟动态内存空间失败即tmp指针指向的内存空间为空(也可以理解为此时tmp指针为空指针)就用perror函数进行报错,如果我们开辟动态内存空间成功就将tmp指针指向的新内存空间的地址传递给a指针这样就实现了对a指针指向的内存空间的扩建,最后将表示此时内存空间大小的newCapacity赋值给capacity,到此动态内存空间的开辟完成了

③ 关于ps->a[ps->size++] = x的解释:因为是尾插,而在a指针指向的空间中a[size]其实就代表了最后一个数的位置,我们想要在这个数之和再插入一个数x,那么x的位置就应该是size+1,所以我们将[]内的内容写成了[ps->size++]即获取此时内存空间中元素的个数后再++,此时a指针指向下标为size++的位置,然后将要插入的数x赋值到该内存空间中(emm差强人意的解释,应该也能懂)

顺序表的头插

//头插
void SLPushFront(SL* ps, SLDataType x) {
  if (ps == NULL && SLIsEmpty(ps) != NULL)
  {
    return;
  }
  SLCheckCapacity(ps);
  //空间足够,原先数据后移一位
  for (size_t i = ps->size; i > 0; i--)
  {
    ps->a[i] = ps->a[i - 1];//当i=1时表示ps->a[1] = ps->a[0],数组的第一个数字被赋值给了第二个数字
  }
  ps->a[0] = x;  //将要插入的内容插入a[0]的位置
  ps->size++;//此时的数组总个数增加一个
}

①关于ps->a[i] = ps->a[i-1]的解释:for循环的意义应该不用过多解释了,ps->a[i] = ps->a[i-1]的意思也很简单就是让前一个数赋值给后一个数然后最后会将a[0]的位置刚好空出来

②关于ps->a[0]=x的解释:这...就是把要插入的数x插入前面循环空出来的a[0]处

顺序表的尾删

//尾删
void SLPopBack(SL* ps) {
  if (ps == NULL )
  {
    return;
  }
  ps->size--;//不需要再进行赋值为0的操作了(ps->a[ps-soze-1] = 0)
}

①关于ps->size--的解释,这里既然是删去最后一个变量的值,也不需要将顺序表的最后一个值赋值为0后再删除,多此一举,直接将用于统计顺序表中有效数据个数的size--即可。

顺序表的头删

//头删
void SLPpopFront(SL* ps) {
  if (ps == NULL)
  {
    return;
  }
  //让后面的数据往前挪动一位
  for (size_t i = 0; i < ps->size - 1;i++)
  {
    //最后一次进来的i的值为size-1
    ps->a[i] = ps->a[i + 1];
  }
  ps->size--;//删除数据当前有效数据个数减一个
}

①关于整个for循环的解释:如果顺序表中有六个数据,那么最后一个数据的下标为size-1,我们想要的是将第一个数据删除,该数据下标为0,我们采用覆盖的办法实现这一目的,将第二个数据赋值给第一个数据,用下标来说就是将ps->a[0] = ps->a[0+1],可以看到的是二者之间的关系是后者比前者多1,所以整体循环的写法就是ps->a[i] = ps->a[i + 1],至于循环的次数问题请看下图:

可以发现六个数据中第六个数据的下标为[size-1=5],所以我们的循环次数应该是size-1次

顺序表的打印

//打印
void SLPrint(SL* ps)
{
  if (ps == NULL )
  {
    return;
  }
  //循环打印
  for (size_t i = 0; i < ps->size; i++)
  {
    printf("%d ", ps->a[i]);
  }
  printf("\n");
}

判断顺序表中有效数据是否非空

//判断顺序表中有效数据是否非空
bool SLIsEmpty(SL* ps)  
{ 
  if (ps == NULL)
  {
    return;
  }
  return ps->size == 0;  
}

在指定位置前插入数据

//在指定位置前插入
void SLInsert(SL* ps,int pos, SLDataType x)
{
  if (ps == NULL &&pos>=0&&pos<=ps->size)
  {
    return;
  }
  SLCheckCapacity(ps);
  //把pos位置及以后的数据往后挪动一位
  //循环条件里i的初始值为size或者size-1都可以,但是不同初始值对应不同的结束条件
  for (size_t i= ps->size;i>pos;i--)
  {
    ps->a[i] = ps->a[i - 1];  //把前面的数据赋值给后面
  }
  ps->a[pos] = x;
  ps->size++;
}

①关于pos的解释:int pos表示我们想要进行数据操作的具体位置

②关于for循环的解释:

如果pos等于3,那么for循环就会变成:for(size_t i = 6;i >3;i--) {ps-a[i] = ps->a[i-1] }

删除指定位置的数据

void SLErase(SL* ps,int pos)
{
  if (ps == NULL && pos >= 0 && pos <= ps->size)
  {
    return;
  }
  SLCheckCapacity(ps);
    //把pos位置以及其之后的数据往前挪动一位
  //最后一次进来的i的数据ps->size-2
  for (int i = pos; i < ps->size - 1; i++)
  {
    ps->a[i] = ps->a[i + 1];
  }
  ps->size--;
}

~不再做过多解释了~

在指定位置查找数据

bool SLFind(SL* ps, SLDataType x)
{
  if (ps == NULL  )
  {
    return;
  }
  for (int i = 0; i < ps->size; i++)
  {
    if (ps->a[i] == x)
    {
      return true;
    }
  }
  return false;
}

~不再做过多解释了~

SeqList.h文件

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
//更改类型为通讯录数据类型
typedef int SLDataType;  //这里将int类型成名为了SLDataType,更换类型时只需要将int换成其它类型即可,它与int类型有着相同的语义和内存表示只是
struct SeqList
{
  SLDataType* a;  //在顺序表中,a 指向存储数据的内存块的起始位置
  int size;     //顺序表中有效的数据个数
  int capacity;   //顺序表中当前空间大小
};  //为结构体重新取名为SL    //结构体类型,结构体的重命名,括号后面的那个到底代表什么?
typedef struct SeqList SL;
//在顺序表中,底层逻辑是使用数组来存储数据。成员变量 a 可以被看作是数组的首元素地址,通过指针 a 可以访问数组中的元素。
//size 表示顺序表中有效的数据个数,它可以看作是数组的元素个数。在顺序表中,只有前 size 个元素是有效的,后面的元素是无效的或未使用的空间。
//capacity 表示顺序表中当前的空间大小,它可以看作是数组的容量。它表示了顺序表当前能够容纳的元素个数。当顺序表中的元素个数达到 capacity 时,可能需要进行扩容操作,以便存储更多的元素。
//所以,可以将 a 视为数组的首元素地址,size 视为数组的元素个数,capacity 视为数组的空间大小。通过操作指针 a,可以访问和操作数组中的元素。
//顺序表的初始化
void SLInit(SL* ps);
//顺序表的释放
void SLDestroy(SL* ps);
//头部/尾部 插入/删除
void SLPushBcak(SL* ps, SLDataType x);
void SLPushFront(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPpopFront(SL* ps);
//打印函数
void SLPrint(SL* ps);
bool SLIsEmpty(SL* ps);
//在指定位置前插入
void SLInsert(SL* ps, int pos, SLDataType x);
//在指定位置删除
void SLErase(SL* ps, int pos);
//在指定位置查找
bool SLFind(SL* ps, SLDataType x);

test.c文件

#include "SeqList.h"
void SLInit(SL* ps) {
  ps->a = NULL;         //ps->a表示指向结构体SL中的成员变量的指针
  ps->size = ps->capacity = 0;  
}
void SLDestroy(SL* ps) {
  if (ps->a)
    free(ps->a);    //释放
  ps->a = NULL;
  ps->size = ps->capacity = 0;
}
//扩容函数
void SLCheckCapacity(SL* ps)
{
  if (ps->size == ps->capacity)//如果当两个值相等时旧证明再要进行插入操作就需要进行开辟新的位置
  {
    int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;//因为初始化时capacity已经被初始化为0了
    SLDataType* tmp = (SLDataType*)realloc(ps->a, newCapacity * 2 * sizeof(SLDataType));
    if (tmp == NULL)//防止扩容失败
    {
      perror("realloc fail\n");
      return 1;
    }
    ps->a = tmp;
    ps->capacity *= newCapacity;
  }
}
//尾插
void SLPushBcak(SL* ps, SLDataType x) //传递结构体指针o
{
  if (ps == NULL )
  {
    return;
  }
  //1、空间不够,扩容(一般以2倍或者1.5倍进行扩容)
  SLCheckCapacity(ps);
  //2、空间足够,直接尾插
  ps->a[ps->size++] = x;  //直接把它当成数组更容易理解
}
//头插
void SLPushFront(SL* ps, SLDataType x) {
  if (ps == NULL)
  {
    return;
  }
  SLCheckCapacity(ps);
  //空间足够,原先数据后移一位
  for (size_t i = ps->size; i > 0; i--)
  {
    ps->a[i] = ps->a[i - 1];//当i=1时表示ps->a[1] = ps->a[0],数组的第一个数字被赋值给了第二个数字
  }
  ps->a[0] = x;  //将要插入的内容插入a[0]的位置
  ps->size++;//此时的数组总个数增加一个
}
//尾删
void SLPopBack(SL* ps) {
  if (ps == NULL )
  {
    return;
  }
  ps->size--;//不需要再进行赋值为0的操作了(ps->a[ps-soze-1] = 0)
}
//头删
void SLPpopFront(SL* ps) {
  if (ps == NULL)
  {
    return;
  }
  //让后面的数据往前挪动一位
  for (size_t i = 0; i < ps->size - 1;i++)
  {
    //最后一次进来的i的值为size-1
    ps->a[i] = ps->a[i + 1];
  }
  ps->size--;//删除数据当前有效数据个数减一个
}
//打印
void SLPrint(SL* ps)
{
  if (ps == NULL )
  {
    return;
  }
  //循环打印
  for (size_t i = 0; i < ps->size; i++)
  {
    printf("%d ", ps->a[i]);
  }
  printf("\n");
}
//判断顺序表中有效数据非空
bool SLIsEmpty(SL* ps)  
{ 
  if (ps == NULL)
  {
    return;
  }
  return ps->size == 0;  
}
//在指定位置前插入
void SLInsert(SL* ps,int pos, SLDataType x)
{
  if (ps == NULL &&pos>=0&&pos<=ps->size)
  {
    return;
  }
  SLCheckCapacity(ps);
  //把pos位置及以后的数据往后挪动一位
  //循环条件里i的初始值为size或者size-1都可以,但是不同初始值对应不同的结束条件
  for (size_t i= ps->size;i>pos;i--)
  {
    ps->a[i] = ps->a[i - 1];
  }
  ps->a[pos] = x;
  ps->size++;
}
void SLErase(SL* ps,int pos)
{
  if (ps == NULL && pos >= 0 && pos <= ps->size)
  {
    return;
  }
  SLCheckCapacity(ps);
  //最后一次进来的i的数据ps->size-2
  for (int i = pos; i < ps->size - 1; i++)
  {
    ps->a[i] = ps->a[i + 1];
  }
  ps->size--;
}
bool SLFind(SL* ps, SLDataType x)
{
  if (ps == NULL)
  {
    return;
  }
  for (int i = 0; i < ps->size; i++)
  {
    if (ps->a[i] == x)
    {
      return true;
    }
  }
  return false;
}

SeqList.c文件

#include "SeqList.h"
void SLTest()
{
  SL sl;
  SLInit(&sl);  //对动态顺序表的初始化
  //顺序表尾插
  SLPushBcak(&sl, 1);
  SLPushBcak(&sl, 2);
  SLPushBcak(&sl, 3);
  SLPushBcak(&sl, 4);//1 2 3 4
  SLPrint(&sl);
  //顺序表头插
  SLPushFront(&sl, 5);//5 1 2 3 4
  SLPushFront(&sl, 6);//6 5 1 2 3 4 
  SLPushFront(&sl, 7);//7 6 5 1 2 3 4
  SLPrint(&sl);
  //尾删
  SLPopBack(&sl);
  SLPrint(&sl);
  SLPopBack(&sl);
  SLPrint(&sl);
  //在指定位置前插入数据
  SLInsert(&sl, 1, 8);
  SLPrint(&sl);
  //删除指定位置的数据
  SLErase(&sl, 2);
  bool findRet = SLFind(&sl, 8);
  if (findRet) {
    printf("找到了!\n");
    SLPrint(&sl);
  }
  else
  {
    printf("没找到!\n");
  }
  SLDestroy(&sl);
}
int main()
{
  SLTest();
  return 0;
}

通讯录的实现:

通讯录底层就是顺序表

看似是在通讯录中增加的数据,实际上是在通讯录中增加的:

下面是所需要的所有文件:(自行理解吧┭┮﹏┭┮)

Contact.h文件:

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include "SeqList.h"
#define NAME_MAX 100
#define SEX_MAX 10
#define TEL_MAX 15
#define ADDR_MAX 100
//创建保存联系人数据的结构
struct ContactInfo {
  //采用定长数组(长也长不到哪里去)
  char name[NAME_MAX];
  char sex[SEX_MAX];
  int age;
  char tel[TEL_MAX];
  char addr[ADDR_MAX];
};
typedef struct ContactInfo CInfo;
//通讯录的底层是顺序表来实现,给顺序表套了一个壳子,后面的pcon其实就相当于ps
typedef struct SeqList contact;
//通讯录的初始化
void ContactInit(contact* pcon);
//通讯录的销毁
void ContactDestory(contact* pcon);
//添加联系人
void ContactAdd(contact* pcon);
//删除联系人
void ContactDel(contact* pcon);
//修改联系人
void ConatactModify(contact* pcon);
//查看通讯录
void ContactShow(contact* pcon);
//查找指定联系人
void ContactFind(contact* pcon);
//通讯录的展示
void ContactShow(contact* pcon);
//通讯录的历史数据的读取
void LoadContact(contact* pcon);
//通讯录的数据存储
void SaveContact(contact* pcon);

SeqList.h文件:

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include "Contact.h"
//更改顺序表类型为通讯录数据类型
typedef struct ContactInfo SLDataType;//让结构体类型作为顺序表的数据类型(套娃)这样就可以在顺序表中使用结构体类型
struct SeqList
{
  SLDataType* a;  //在顺序表中,a 指向存储数据的内存块的起始位置
  int size;     //顺序表中有效的数据个数
  int capacity;   //顺序表中当前空间大小
};  //为结构体重新取名为SL    //结构体类型,结构体的重命名,括号后面的那个到底代表什么?
typedef struct SeqList SL;
//这些解释是gpt说的哈,简单看一下就行......
//在顺序表中,底层逻辑是使用数组来存储数据。成员变量 a 可以被看作是数组的首元素地址,通过指针 a 可以访问数组中的元素。
//size 表示顺序表中有效的数据个数,它可以看作是数组的元素个数。在顺序表中,只有前 size 个元素是有效的,后面的元素是无效的或未使用的空间。
//capacity 表示顺序表中当前的空间大小,它可以看作是数组的容量。它表示了顺序表当前能够容纳的元素个数。当顺序表中的元素个数达到 capacity 时,可能需要进行扩容操作,以便存储更多的元素。
//所以,可以将 a 视为数组的首元素地址,size 视为数组的元素个数,capacity 视为数组的空间大小。通过操作指针 a,可以访问和操作数组中的元素。
//顺序表的初始化
void SLInit(SL* ps);
//顺序表的释放
void SLDestroy(SL* ps);
//头部/尾部 插入/删除
void SLPushBack(SL* ps, SLDataType x);
void SLPushFront(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPpopFront(SL* ps);
//打印函数
void SLPrint(SL* ps);
bool SLIsEmpty(SL* ps);
//在指定位置前插入
void SLInsert(SL* ps, int pos, SLDataType x);
//在指定位置删除
void SLErase(SL* ps, int pos);
//在指定位置查找
bool SLFind(SL* ps, SLDataType x);

test.c文件:

#include "SeqList.h"
void contact01()
{
  contact con;
  //通讯录初始化
  ContactInit(&con);
  //往通讯录中插入数据
  ContactAdd(&con);
  ContactShow(&con);
  ContactDel(&con);
  ContactShow(&con);
  ContactFind(&con);
  //通讯录的销毁
  ContactDestory(&con);
}
void menu() {
  printf("******************通讯录******************\n");
  printf("*******1、添加联系人  2、删除联系人*******\n");
  printf("*******3、修改联系人  4、查找联系人*******\n");
  printf("*******5、查看通讯录        0、退出*******\n");
  printf("******************************************\n");
}
int main()
{
  int op = -1;
  //定义一个通讯录
  contact con;
  ContactInit(&con);
  do {
    menu();
    printf("请选择您的操作:\n");
    scanf("%d", &op);
    switch (op)
    {
    case 1:
      ContactAdd(&con);
      break;
    case 2:
      ContactDel(&con);
      break;
    case 3:
      ContactModify(&con);
      break;
    case 4:
      ContactFind(&con);
      break;
    case 5:
      ContactShow(&con);
      break;
    case 0:
      printf("感谢使用~\n");
      break;
    default:
      printf("输入非法,请重新输入\n");
      break;
    }
  } while (op != 0);//op为0退出通讯录
  ContactDestory(&con);
  return 0;
}

Contact.c文件:

#include "Contact.h"
#include "SeqList.h"
//加载历史数据
void LoadContact(contact* pcon) {
  FILE* pf = fopen("contact.txt", "rb");
  if (pf == NULL) {
    perror("fopen error!\n");
    return;
  }
  //循环读取⽂件数据
  CInfo info;
  while (fread(&info, sizeof(CInfo), 1, pf))
  {
    SLPushBack(pcon, info);
  }
  printf("历史数据导入通讯录成功!\n");
}
//存储通讯录数据
void SaveContact(contact* pcon) {
  FILE* pf = fopen("contact.txt", "wb");
  if (pf == NULL) {
    perror("fopen error!\n");
    return;
  }
  //将通讯录数据写⼊⽂件
  for (int i = 0; i < pcon->size; i++)
  {
    fwrite(pcon->a + i, sizeof(CInfo), 1, pf);
  }
  printf("通讯录数据保存成功!\n");
}
void ContactInit(contact* pcon) {
  //借用之前的顺序表的初始化对通讯录进行初始化
  SLInit(pcon);
  LoadContact(pcon);
}
void ContactDestory(contact* pcon) {
  SaveContact(pcon);
  //借用之前的顺序表的销毁对通讯录进行销毁
  SLDestroy(pcon);
}
//添加联系人
void ContactAdd(contact* pcon)
{
  //还是套娃
  CInfo info;
  printf("请输入联系人姓名:\n");
  scanf("%s", info.name);
  printf("请输入联系人的性别:\n");
  scanf("%s", info.sex);
  printf("请输入联系人的年龄:\n");
  scanf("%d", &info.age);
  printf("请输入联系人的电话:\n");
  scanf("%s", info.tel);
  printf("请输入联系人的地址:\n");
  scanf("%s", info.addr);
  //联系人数据都获取到了,并保存到了结构体info成员变量中
  //往通讯录(顺序表)中插入数据
  SLPushBack(pcon, info);
}
//查找函数
int FindByName(contact* pcon, char name[])
{
  for (int i = 0; i < pcon->size; i++)
  {
    if (strcmp(pcon->a[i].name, name) == 0)
    {
      return i;
    }
  }
  return -1;
}
//删除联系人
void ContactDel(contact* pcon)
{
  //直接强制要求用户使用联系人名称来查找
  printf("请输入要删除的用户名称:\n");
  char name[NAME_MAX];
  scanf("%s", name);
  int find = FindByName(pcon, name);
  if (find < 0)
  {
    printf("要删除的联系人不存在!\n");
    return;
  }
  //找到了,要删除findidx位置的数据
  SLErase(pcon, find);
}
//修改联系人
void ContactModify(contact* pcon) {
  char name[NAME_MAX];
  printf("请输入要修改的用户名称:\n");
  scanf("%s", name);
  //获取到的通讯里(顺序表)下标的位置
  int find = FindByName(pcon, name);
  if (find < 0)
  {
    printf("要修改的用户不存在!\n");
    return;
  }
  printf("请输入新的用户名称:\n");
  scanf("%s", pcon->a[find].name);
  printf("请输入新的用户性别:\n");
  scanf("%s", pcon->a[find].sex);
  printf("请输入新的用户年龄:\n");
  scanf("%d", pcon->a[find].age);
  printf("请输入新的用户电话:\n");
  scanf("%s", pcon->a[find].tel);
  printf("请输入新的用户地址:\n");
  scanf("%s", pcon->a[find].addr);
  printf("修改成功!\n");
}
//查看通讯录
void ContactShow(contact* pcon)
{
  //打印通讯录所有的数据
  //先打印表头文字
  printf("%s %s %s %s %s\n", "姓名", "性别", "年龄", "电话", "住址");
  for (int i = 0; i < pcon->size; i++)
  {
    printf("%-4s %-4s %-4d %-4s %-4s\n",
      pcon->a[i].name,
      pcon->a[i].sex,
      pcon->a[i].age,
      pcon->a[i].tel,
      pcon->a[i].addr
    );
  }
}
//查找指定联系人
void ContactFind(contact* pcon) {
  char name[NAME_MAX];
  printf("请输入要查找的用户名称:\n");
  scanf("%s", &name);
  int find = FindByName(pcon, name);
  if (find < 0)
  {
    printf("该联系人不存在!\n");
  }
  //打印当前找到的联系人
  printf("%-4s %-4s %-4d %-4s %-4s\n",
    pcon->a[find].name,
    pcon->a[find].sex,
    pcon->a[find].age,
    pcon->a[find].tel,
    pcon->a[find].addr
  );
}

SeqList.c文件:

#include "SeqList.h"
void SLInit(SL* ps) {
  ps->a = NULL;         //ps->a表示指向结构体SL中的成员变量的指针
  ps->size = ps->capacity = 0;
}
void SLDestroy(SL* ps) {
  if (ps->a)
    free(ps->a);    //释放
  ps->a = NULL;
  ps->size = ps->capacity = 0;
}
//扩容函数
void SLCheckCapacity(SL* ps)
{
  if (ps->size == ps->capacity)//如果当两个值相等时旧证明再要进行插入操作就需要进行开辟新的位置
  {
    int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;//因为初始化时capacity已经被初始化为0了
    SLDataType* tmp = (SLDataType*)realloc(ps->a, newCapacity * 2 * sizeof(SLDataType));
    if (tmp == NULL)//防止扩容失败
    {
      perror("realloc fail\n");
      return 1;
    }
    ps->a = tmp;
    ps->capacity *= newCapacity;
  }
}
//尾插
void SLPushBack(SL* ps, SLDataType x) //传递结构体指针o
{
  if (ps == NULL)
  {
    return;
  }
  //1、空间不够,扩容(一般以2倍或者1.5倍进行扩容)
  SLCheckCapacity(ps);
  //2、空间足够,直接尾插
  ps->a[ps->size++] = x;  //直接把它当成数组更容易理解
}
//头插
void SLPushFront(SL* ps, SLDataType x) {
  if (ps == NULL)
  {
    return;
  }
  SLCheckCapacity(ps);
  //空间足够,原先数据后移一位
  for (size_t i = ps->size; i > 0; i--)
  {
    ps->a[i] = ps->a[i - 1];//当i=1时表示ps->a[1] = ps->a[0],数组的第一个数字被赋值给了第二个数字
  }
  ps->a[0] = x;  //将要插入的内容插入a[0]的位置
  ps->size++;//此时的数组总个数增加一个
}
//尾删
void SLPopBack(SL* ps) {
  if (ps == NULL)
  {
    return;
  }
  ps->size--;//不需要再进行赋值为0的操作了(ps->a[ps-soze-1] = 0)
}
//头删
void SLPpopFront(SL* ps) {
  if (ps == NULL)
  {
    return;
  }
  //让后面的数据往前挪动一位
  for (size_t i = 0; i < ps->size - 1; i++)
  {
    //最后一次进来的i的值为size-1
    ps->a[i] = ps->a[i + 1];
  }
  ps->size--;//删除数据当前有效数据个数减一个
}
//打印
void SLPrint(SL* ps)
{
  if (ps == NULL)
  {
    return;
  }
  //循环打印
  for (size_t i = 0; i < ps->size; i++)
  {
    printf("%d ", ps->a[i]);
  }
  printf("\n");
}
//判断顺序表中有效数据非空
bool SLIsEmpty(SL* ps)
{
  if (ps == NULL)
  {
    return;
  }
  return ps->size == 0;
}
//在指定位置前插入
void SLInsert(SL* ps, int pos, SLDataType x)
{
  if (ps == NULL && pos >= 0 && pos <= ps->size)
  {
    return;
  }
  SLCheckCapacity(ps);
  //把pos位置及以后的数据往后挪动一位
  //循环条件里i的初始值为size或者size-1都可以,但是不同初始值对应不同的结束条件
  for (size_t i = ps->size; i > pos; i--)
  {
    ps->a[i] = ps->a[i - 1];
  }
  ps->a[pos] = x;
  ps->size++;
}
void SLErase(SL* ps, int pos)
{
  if (ps == NULL && pos >= 0 && pos <= ps->size)
  {
    return;
  }
  SLCheckCapacity(ps);
  //最后一次进来的i的数据ps->size-2
  for (int i = pos; i < ps->size - 1; i++)
  {
    ps->a[i] = ps->a[i + 1];
  }
  ps->size--;
}
//先隐藏着吧这个在通讯录里面没用
//bool SLFind(SL* ps, SLDataType x)
//{
//  if (ps == NULL)
//  {
//    return;
//  }
//  for (int i = 0; i < ps->size; i++)
//  {
//    if (ps->a[i] == x)
//    {
//      return true;
//    }
//  }
//  return false;
//}

效果演示:

哪点看不懂的可以在评论区留言┭┮﹏┭┮,有空会写详细的解释

顺序表经典算法OJ题:

移除元素:

题目:给你一个数组nums和一个值val,你需要原地移除所有数值等于val的元素,并返回移除后数组的新长度。不能使用额外的数组空间,你必须仅使用0(1)额外空间并原地修改数组,元素的顺序可以改变,你不需要考虑数组中超出新长度后面的元素。

伪双指针法:

#include <stdio.h>
int rempveElement(int* nums, int numSize, int val)
{
  int src = 0, dst = 0;
  while (src < numSize) {
    if (nums[src] == val)
    {
      src++;
    }
    else
    {
      nums[dst] = nums[src];
      dst++;
      src++;
    }
  }
  return dst;
}
int main()
{
  int arr[4] = { 3,2,2,3 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  int num = rempveElement(arr, sz, 3);
  printf("%d", num);
  return 0;
}

合并两个数组:

题目:给你两个按非递减顺序排列的整数数组nums1和nums2,另有两个整数m和n,分别表示nums1和nums2中的元素数目,请你合并nums2到nums1中,使合并后的数组同样按照非递减顺序排列。

注意:最终合并后的数组不应由函数返回,而是存储在数组nums1中,为了应对这种情况,nums1的初始长度伪m+n,其中m个元素表示应合并的元素,后n给元素为0,应忽略。nums2的程度为n。

void merge(int* nums1, int numSize, int m, int* nums2, int num2Size, int n)
{
  int l1 = m - 1, l2 = n - 1;
  int l3 = m + n - 1;
  while (l1 >= 0 && l2 >= 0)
  {
    if (nums1[l1] > nums2[l2])
    {
      nums1[l3--] = nums1[l1--];
    }
    else
    {
      nums1[l3--] = nums2[l2--];
    }
  }
  while (l2 >= 0)
  {
    nums1[l3--] = nums2[l2--];
  }
}
int main()
{
  int nums1[] = { 1, 2, 3, 0, 0, 0 };
  int m = 3;
  int nums2[] = { 2, 5, 6 };
  int n = 3;
  int sz1 = sizeof(nums1) / sizeof(nums1[0]);
  int sz2 = sizeof(nums2) / sizeof(nums2[0]);
  merge(nums1, sz1, m, nums2, sz2, n);
  for (int i = 0; i < m + n; i++)
  {
    printf("%d ", nums1[i]);
  }
  printf("\n");
  return 0;
}

顺序表的问题及思考

1. 中间/头部的插⼊删除,时间复杂度为O(N)

2. 增容需要申请新空间,拷⻉数据,释放旧空间。会有不⼩的消耗。

3. 增容⼀般是呈2倍的增⻓,势必会有⼀定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插⼊了5个数据,后⾯没有数据插⼊了,那么就浪费了95个数据空间。

思考:如何解决以上问题呢?

~over~

相关文章
|
19天前
|
存储 C语言
【数据结构】顺序表(c语言实现)(附源码)
本文介绍了线性表和顺序表的基本概念及其实现。线性表是一种有限序列,常见的线性表有顺序表、链表、栈、队列等。顺序表是一种基于连续内存地址存储数据的数据结构,其底层逻辑是数组。文章详细讲解了静态顺序表和动态顺序表的区别,并重点介绍了动态顺序表的实现,包括初始化、销毁、打印、增删查改等操作。最后,文章总结了顺序表的时间复杂度和局限性,并预告了后续关于链表的内容。
50 3
|
1月前
|
存储 C语言
探索C语言数据结构:利用顺序表完成通讯录的实现
本文介绍了如何使用C语言中的顺序表数据结构实现一个简单的通讯录,包括初始化、添加、删除、查找和保存联系人信息的操作,以及自定义结构体用于存储联系人详细信息。
19 2
|
1月前
|
C语言
链式顺序表实现(C语言描述)
本文介绍了如何在C语言中实现链式顺序表,包括数据结构的定义、节点的创建、数据的插入和删除以及链表的打印和销毁。
38 2
|
1月前
|
C语言
顺序表数组法构建(C语言描述)
如何使用C语言通过数组方法构建有序顺序表,包括顺序表的创建、插入、删除和打印等。
18 2
|
1月前
|
存储 C语言
手把手教你用C语言实现通讯录管理系统
手把手教你用C语言实现通讯录管理系统
|
2月前
|
存储 C语言 C++
数据结构基础详解(C语言) 顺序表:顺序表静态分配和动态分配增删改查基本操作的基本介绍及c语言代码实现
本文介绍了顺序表的定义及其在C/C++中的实现方法。顺序表通过连续存储空间实现线性表,使逻辑上相邻的元素在物理位置上也相邻。文章详细描述了静态分配与动态分配两种方式下的顺序表定义、初始化、插入、删除、查找等基本操作,并提供了具体代码示例。静态分配方式下顺序表的长度固定,而动态分配则可根据需求调整大小。此外,还总结了顺序表的优点,如随机访问效率高、存储密度大,以及缺点,如扩展不便和插入删除操作成本高等特点。
196 5
|
2月前
|
存储 算法 C语言
C语言手撕数据结构代码_顺序表_静态存储_动态存储
本文介绍了基于静态和动态存储的顺序表操作实现,涵盖创建、删除、插入、合并、求交集与差集、逆置及循环移动等常见操作。通过详细的C语言代码示例,展示了如何高效地处理顺序表数据结构的各种问题。
|
2月前
|
网络协议 C语言
C语言 网络编程(十四)并发的TCP服务端-以线程完成功能
这段代码实现了一个基于TCP协议的多线程服务器和客户端程序,服务器端通过为每个客户端创建独立的线程来处理并发请求,解决了粘包问题并支持不定长数据传输。服务器监听在IP地址`172.17.140.183`的`8080`端口上,接收客户端发来的数据,并将接收到的消息添加“-回传”后返回给客户端。客户端则可以循环输入并发送数据,同时接收服务器回传的信息。当输入“exit”时,客户端会结束与服务器的通信并关闭连接。
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
8天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
24 6