顺序表之(条条有注释助你吃透顺序表以及基于顺序表实现的通讯录)

顺序表

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

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

静态顺序表

格式:

typedef struct 静态顺序表名

{

ElementType data[MaxSize]; // 存储元素的数组

int length; // 当前顺序表的长度

} ;

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

动态顺序表

格式:

typedef struct 动态顺序表名

{

ElementType *data; // 存储元素的指针

int length; // 当前顺序表的长度

int capacity; // 当前顺序表的容量

} ;

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

动态顺序表的实现通常包含两个部分:一个指向动态分配内存块的指针(通常称为data或elements),以及一个用于记录顺序表当前元素个数和容量的变量(通常称为size和capacity)。当创建动态顺序表时,会动态分配一块内存空间,并将 data 指针指向这个内存块。这个内存块可以根据需要进行动态调整,以适应顺序表的容量需求

动态顺序表实现:

我们要实现一个动态顺序需要经历以下两个主要部分:

  • 顺序表的初始化和释放
  • 顺序表中元素的插入和删除

我们这次仍然采用多文件的方式:

首先分别创建头文件SeqList.h、测试文件test.c(主函数所在文件)、功能实现函数SeqList.c

主函数的创建

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结构体类型的变量sl,当然你也可以叫它struct SeqList结构体类型的变量sl,只不过后者读起来麻烦。

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

顺序表的初始化

SeqList.h文件:

#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的类型时只需要

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

SeqList.c文件:

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,目的就是进行初始化

顺序表的释放

SeqList.h文件:

//顺序表的释放
void SLDestroy(SL* ps);

SeqList.c文件:

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指针设置为空指针就行

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

顺序表的尾部插入

SeqList.h文件:

//尾部插入
void SLPushBcak(SL* ps, SLDataType x);

SeqList.c文件:

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

SeqList.c文件:

//头插
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 SLPpopBack(SL* ps);

SeqList.c文件:

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

SeqList.c文件:

//头删
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);

SeqList.c文件:

//打印
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);

SeqList.c文件:

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

在指定位置前插入数据

//在指定位置前插入
void SLInsert(SL* ps, int pos, SLDataType x);

SeqList.c文件:

//在指定位置前插入
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);

SeqList.c文件:

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);

SeqList.c文件:

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
#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);

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;


//在顺序表中,底层逻辑是使用数组来存储数据。成员变量 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 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 ContactInit(contact* pcon) {
	//借用之前的顺序表的初始化对通讯录进行初始化
	SLInit(pcon);
}
void ContactDestory(contact* pcon) {
	//借用之前的顺序表的初始化对通讯录进行初始化
	SLDestroy(pcon);
}

//添加联系人
void ContactAdd(contact* pcon) 
{
	//还是套娃
	CInfo info;
	printf("请输入联系人姓名:\n");
	scanf("%s",info.name);
	printf("请输入联系人的性别:\n");
	scanf("%s", info.sex);
	printf("请输入联系人的年龄:\n");
	scanf("%s", &info.age);
	printf("请输入联系人的电话:\n");
	scanf("%s", info.tel);
	printf("请输入联系人的地址:\n");
	scanf("%s", info.addr);

	//联系人数据都获取到了,并保存到了结构体info成员变量中
	//往通讯录(顺序表)中插入数据
	SLPushBcak(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 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;
//}

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

顺序表经典算法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~

相关推荐
小屁孩大帅-杨一凡40 分钟前
java后端请求想接收多个对象入参的数据
java·开发语言
m0_656974741 小时前
C#中的集合类及其使用
开发语言·c#
java1234_小锋1 小时前
使用 RabbitMQ 有什么好处?
java·开发语言
wjs20241 小时前
R 数据框
开发语言
肘击鸣的百k路1 小时前
Java 代理模式详解
java·开发语言·代理模式
捕鲸叉1 小时前
MVC(Model-View-Controller)模式概述
开发语言·c++·设计模式
wrx繁星点点1 小时前
享元模式:高效管理共享对象的设计模式
java·开发语言·spring·设计模式·maven·intellij-idea·享元模式
真的想不出名儿1 小时前
Java基础——反射
java·开发语言
努力编程的阿伟2 小时前
【Java SE语法】抽象类(abstract class)和接口(interface)有什么异同?
java·开发语言
包饭厅咸鱼2 小时前
QML----复制指定下标的ListModel数据
开发语言·数据库