【极简数据结构】快速了解并实现顺序表,速通玩家的最爱

顺序表目录

  • 前言
  • 一、线性表
  • 二、顺序表
    • 1.顺序表的概念
    • 2.接口函数
      • 顺序表 初始化
      • 顺序表 尾插
      • 顺序表 打印
      • 顺序表 销毁
      • 顺序表 尾删
      • 顺序表 头插 和 顺序表 扩容
      • ⚡优化顺序表 尾删
      • 顺序表 头删
      • 顺序表 查找
      • 顺序表 任意pos位置插入
      • 对头插函数和尾插函数的优化
      • 顺序表 任意pos位置删除
      • 对头删函数和尾删函数的优化
  • 四、完整代码
    • SeqList.h
    • SeqList.c
    • Test.c

前言

从0快速实现顺序表。


一、线性表

线性表(Linear list)是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表有:顺序表、链表、栈、队列、字符串…
② 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物 理上存储时,通常以数组和链式结构的形式存储。

【极简数据结构】快速了解并实现顺序表,速通玩家的最爱_第1张图片


二、顺序表

1.顺序表的概念

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

顺序表顾名思义,数据是按着顺序连续存储的。
顺序表分为

  1. 静态顺序表

使用定长数组存储元素:

#define N 7
typedef int SLDataType;
typedef struct SeqList
{
    SLDataType array[N];  //定长数组
    size_t size;          //有效数据的个数
} SL;

【极简数据结构】快速了解并实现顺序表,速通玩家的最爱_第2张图片

静态顺序表的特点:如果满了就不让插入
静态顺序表的缺点:设置的长度是多少呢?不能准确确定。N给小了不够用;N给大了就显得浪费

  1. 动态顺序表

使用动态开辟的数组存储元素:

typedef int SLDataType;
 
typedef struct SeqList
{
    SLDataType* array;    //指向动态开辟的数组
    size_t size;          //有效数据的个数
    size_t capacity;      //容量空间的大小
} SL;

【极简数据结构】快速了解并实现顺序表,速通玩家的最爱_第3张图片

动态顺序表可根据需要动态地分配空间,因此在现实中广泛使用。


2.接口函数

接口函数: 某个模块写了(主要)给其它模块用的函数。
注意:
① 接口函数的命名风格跟着STL走,便于后期学习STL
② 模块化思想:SeqList.h 头文件存放 结构体、函数的声明; SeqList.c 源文件存放 函数的定义;Test.c 源文件 用于测试。

主要的接口函数有:
接口函数采用址传递方式

SeqList.h
#pragma once
#include
#include
#include

//动态顺序表
typedef int SLDataType;
typedef struct SeqList
{
    SLDataType* array;    //指向动态开辟的数组
    size_t size;          //有效数据的个数
    size_t capacity;      //容量空间的大小
} SL;

/*接口函数*/
//顺序表 初始化
void SeqListInit(SL* psl);
//顺序表 尾插
void SeqListPushBack(SL* psl, SLDateType x);
//顺序表 打印
void SeqListPrint(SL* psl);
//顺序表 销毁
void SeqListDestory(SL* psl);
//顺序表 尾删
void SeqListPopBack(SL* psl);
//顺序表 头插
void SeqListPushFront(SL* psl,SLDateType x);
//顺序表 判断扩容
void SeqListCheckCapacity(SL* psl);
//顺序表 头删
void SeqListPopFront(SL* psl);
//顺序表 查找
int SeqListFind(SL* psl,SLDateType x);
//顺序表 任意pos位置插入
int SeqListInsert(SL* psl, int pos, SLDateType x);
//顺序表 任意pos位置删除
void SeqListEarse(SL* psl, int pos);

解读:#pragma once 为了避免同一个头文件被多次包含。

顺序表 初始化

SeqList.c
//顺序表 初始化
void SeqListInit(SL* psl)
{
	psl->array = NULL;
	psl->size = psl->capacity = 0;
}

解读:

顺序表 尾插

尾插要考虑三种情况:

  1. 整个顺序表没有空间
  2. 空间已满,则扩容
  3. 空间是足够的,直接插入数据即可

总结: 尾插需要考虑是否需要扩容

SeqList.c
//顺序表 尾插
void SeqListPushBack(SL* psl, SLDataType x)
{		
	//判断是否需要扩容
	if (psl->size == psl->capacity)
	{
		int newcapacity = psl->capacity == 0 ? 4 : psl->capacity*2;
		//realloc:若原空间为空,就等于malloc。调整为newcapacity个SLDataType大小的空间
		SLDataType* tmp = (SLDataType*)realloc(psl->array, new_capacity * sizeof(SLDataType));
        //防止realloc翻车,也可以用assert(tmp);断言
		if (tmp == NULL)
		{
			printf("realloc failed!\n");
			exit(-1);
		}
		//更新
		psl->array = tmp;
		psl->capacity = newcapacity;
	}
	//插入
	psl->array[psl->size] = x;
	psl->size++;
}

解读:
① 扩容时,扩大为原来capacity的2倍(2倍是比较合理、折中的倍数,你也可以选择其他的倍数)
② 为了避免realloc失败返回NULL导致后续会访问空,我们创建了临时指针tmp接收,并对其判断。

顺序表 打印

SeqList.c
//顺序表 打印
void SeqListPrint(SL* psl)
{
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->array[i]);
	}
	printf("\n");
}

解读: for循环遍历顺序表,并逐一打印。

温馨提示:编写代码要养成写一段测试一段的习惯。切勿等写完再来测试

测试代码:

Test.c
#include "SeqList.h"
void TestSeqList1()
{
	SL sl;
	SeqListInit(&sl);
 
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	SeqListPushBack(&sl, 5);
	SeqListPrint(&SL);
}
 
int main() 
{
	TestSeqList1();
	return 0;
}

在这里插入图片描述
解读: TestSeqList1();模块化测试思想
具体测试方法:【实用调试技巧】,在此不再叙述。一定要多尝试自己测试调试代码。(下文就不再带大家测试了)

顺序表 销毁

注意: 数组是动态开辟的,对于动态开辟的内存空间,我们要养成手动free掉的习惯,否则会有内存泄露的风险。

SeqList.c
//顺序表 销毁
void SeqListDestory(SL* psl)
{
	free(psl->array);
	psl->array = NULL;
	psl->capacity = psl->size = 0;
}

解读: free掉psl->array指向的内存空间后,要psl->array置空,以防止其变成野指针。

顺序表 尾删

① 尾删就是让psl->size--;(让有效数据减1即可,因为psl->size作为数组下标时是下一个插入数据的位置)
【极简数据结构】快速了解并实现顺序表,速通玩家的最爱_第4张图片

② 尾删要考虑顺序表是否为空的情况,当顺序表都空了还删除会有越界非法访问的安全问题。即尾删要考虑判空

SeqList.c
//顺序表 尾删
void SeqListPopBack(SL* psl)
{
	//1.温柔手法
	/*	if (psl->size > 0)
	{
		psl->size--;
	}*/
	//2.暴力手法
	assert(psl->size > 0);
	psl->size--;
}

解读: 尾插要考虑判空,判空建议用断言方式。

顺序表 头插 和 顺序表 扩容

SeqList.c
//顺序表 扩容
void SeqListCheckCapacity(SL* psl)
{
	//判断是否需要扩容
	if (psl->size == psl->capacity)
	{
		int newcapacity = psl->capacity == 0 ? 4 : psl->capacity*2;
		//realloc:若原空间为空,就等于malloc。调整为newcapacity个SLDataType大小的空间
		SLDataType* tmp = (SLDataType*)realloc(psl->array, new_capacity * sizeof(SLDataType));
        //防止realloc翻车,也可以用assert(tmp);断言
		if (tmp == NULL)
		{
			printf("realloc failed!\n");
			exit(-1);
		}
		//更新
		psl->array = tmp;
		psl->capacity = newcapacity;
	}
}
//顺序表 头插
void SeqListPushFront(SL* psl, SLDateType x)
{
	SeqListCheckCapacity(psl);//复用
	int end = psl->size - 1;//psl->size作为数组下标时是下一个插入数据的位置,所以要减1
	while (end >= 0)
	{
		psl->array[end + 1] = psl->array[end];
		end--;
	}
	//此时,array[0]被挪到后一位
	psl->array[0] = x;
	psl->size++;
}

解读:

插入要考虑是否需要扩容,头插和尾插都需要扩容,我们可以将扩容操作封装为一个SeqListCheckCapacity函数,再在头插和尾插中复用。
② 顺序表的数据是从头开始连续存储的。所以顺序表实现头插,需要将所有元素往后挪动1位。int end = psl->size - 1; 按psl->array[end]、psl->array[end - 1]…的顺序依次往后挪1位

⚡优化顺序表 尾删

在尾插中复用扩容

SeqList.c
//顺序表 尾插
void SeqListPushBack(SL* psl, SLDateType x)
{
	SeqListCheckCapacity(psl);//复用扩容函数
	psl->array[psl->size] = x;
	psl->size++;
}

优化后可读性提高!

顺序表 头删

SeqList.c
//顺序表 头删
void SeqListPopFront(SL* psl)
{
	assert(psl->size > 0);
	int begin = 1;
	while (begin < psl->size)
	{
		psl->array[begin - 1] = psl->array[begin];
		begin++;
	}
	psl->size--;
}

解读:
删除需要考虑判空
int begin = 1; 按psl->array[1]、psl->array[2]…的顺序依次往前挪1位,最后psl->size--;

顺序表 查找

SeqList.c
//顺序表 查找
int SeqListFind(SL* psl,SLDateType x)
{
	//数组下标从0开始
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		if (psl->array[i] == x)
		{
			return i;
		}
	}
	//没找到
	return -1;
}

解读:
比较简单,for循环依次遍历顺序表 若 psl->array[i] == x ,则返回下标 i;没找到就返回 -1
时间复杂度为O(N)。可以尝试写一个二分查找算法的接口函数实现,但前提要排序。

顺序表 任意pos位置插入

前面学习了顺序表的头插和尾插,我们能不能在任意的pos位置插入呢?这得考虑考虑!

【极简数据结构】快速了解并实现顺序表,速通玩家的最爱_第5张图片
因此我们要对pos进行条件限定。另外,插入就得考虑是否需要扩容。

SeqList.c
//顺序表 任意pos位置插入
int SeqListInsert(SL* psl, int pos, SLDateType x)
{
	//插入的pos位置有区域约束

	//1.温柔方法
	/*	if (pos < 0 && pos > psl->size)
	{
		printf("pos invalidd(pos非法!)\n");
		return;
	}*/

	//2.暴力方法
	assert(pos >= 0 && pos <= psl->size);

	//凡是插入,考虑是否需要扩容
	SeqListCheckCapacity(psl);

	//挪数据
	int end = psl->size - 1;
	while (end >= pos)
	{
		psl->array[end + 1] = psl->array[end];
		end--;
	}

	//正式插入
	psl->array[pos] = x;
	psl->size++;
}

解读:
思路类似头插,只是while循环的终止条件发生变化。

对头插函数和尾插函数的优化

SeqListInsert函数可以在任意pos位置插入数据,当然包括了头部和尾部。因此我们可以在头插和尾插函数中复用SeqListInsert函数。真是妙极了!

头插函数复用SeqListInsert函数

//顺序表 头插(复用SeqListInsert函数)
void SeqListPushFront(SL* psl, SLDataType x) 
{
	SeqListInsert(psl, 0, x);
}

尾插函数复用SeqListInsert函数

//顺序表 尾插(复用SeqListInsert函数
void SeqListPushBack(SL* psl, SLDataType x) 
{
	SeqListInsert(psl, psl->size, x);
}

顺序表 任意pos位置删除

① 类似任意pos位置插入,我们也可以在pos位置删除。同样pos位置是有条件限定的,特别的是注意psl->size无有效数据,不可删除。

② 删除就得考虑判空

//顺序表 任意pos位置删除
void SeqListEarse(SL* psl, int pos)
{
	//删除和插入一样:有区域约束,不可任意访问删除
	  
	//1.温柔方法
	/*	if (pos < 0 && pos >= 0)
	{
		printf("pos invalidd!\n");
		return;
	}*/

	//2.暴力处理
	assert(pos >= 0 && pos < psl->size);

	int begin = pos;
	while (begin < psl->size)
	{
		psl->array[begin] = psl->array[begin + 1];
		begin++;
	}
	psl->size--;
}

解读:
思路类似头删。

对头删函数和尾删函数的优化

同样的道理,头删和尾删也可以复用SeqListEarse函数
头删函数复用SeqListEarse函数

//顺序表 头删(复用SeqListEarse函数)
void SeqListPopFront(SL* psl) 
{
	SeqListEarse(psl, 0);
}

尾删函数复用SeqListEarse函数

//顺序表 尾删(复用SeqListEarse函数)
void SeqListPopBack(SL* psl)
{
	SeqListEarse(psl, psl->size - 1);
}

到此,想必速通万家们已经知道如何快速实现顺序表了!
我们把SeqListInsert函数和SeqListEarse函数写了,就相当于把头插尾插、头删尾删写了(复用即可)


四、完整代码

SeqList.h

#pragma once
#include
#include
#include

创建静态顺序表
//#define N 10
//typedef int SLDateType;
//
//typedef struct SeqList
//{
//	SLDateType array[N];//定长数组
//	SLDateType size;//有效个数
//}SL;

//创建动态顺序表
typedef int SLDateType;
typedef struct SeqList
{
	SLDateType* array;//指向动态开辟的数组
	SLDateType size;//有效个数
	SLDateType capacity;//容量空间大小
}SL;

/*接口函数*/

//顺序表 初始化
void SeqListInit(SL* psl);

//顺序表 尾插
void SeqListPushBack(SL* psl, SLDateType x);

//顺序表 打印
void SeqListPrint(SL* psl);

//顺序表 销毁
void SeqListDestory(SL* psl);

//顺序表 尾删
void SeqListPopBack(SL* psl);

//顺序表 头插
void SeqListPushFront(SL* psl,SLDateType x);

//顺序表 判断扩容
void SeqListCheckCapacity(SL* psl);

//顺序表 头删
void SeqListPopFront(SL* psl);

//顺序表 查找
int SeqListFind(SL* psl,SLDateType x);

//顺序表 任意pos位置插入
int SeqListInsert(SL* psl, int pos, SLDateType x);

//顺序表 任意pos位置删除
void SeqListEarse(SL* psl, int pos);

SeqList.c

#include"SeqList.h"

//顺序表 初始化
void SeqListInit(SL* psl)
{
	psl->array = NULL;
	psl->size = psl->capacity = 0;
}

//顺序表 尾插
void SeqListPushBack(SL* psl, SLDateType x)
{
	//插入
	SeqListCheckCapacity(psl);
	psl->array[psl->size] = x;
	psl->size++;
}

//顺序表 打印
void SeqListPrint(SL* psl)
{
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->array[i]);
	}
	printf("\n");
}

//顺序表 销毁
void SeqListDestory(SL* psl)
{
	free(psl->array);
	psl->array = NULL;
	psl->capacity = psl->size = 0;
}

//顺序表 尾删
void SeqListPopBack(SL* psl)
{
	1.温柔手法
	//if (psl->size > 0)
	//{
	//	psl->size--;
	//}
	
	//没有考虑到"删过头"的情况,会非法访问
	psl->array[psl->size - 1] = 0;
	//psl->size--;

	//2.暴力手法
	//assert(psl->size > 0);
	//psl->size--;
	
	//复用SeqListEarse
	SeqListEarse(psl, psl->size - 1);
}

//顺序表 头插
void SeqListPushFront(SL* psl, SLDateType x)
{
	SeqListCheckCapacity(psl);
	int end = psl->size - 1;
	while (end >= 0)
	{
		psl->array[end + 1] = psl->array[end];
		end--;
	}

	//此时,array[0]被挪到后一位
	psl->array[0] = x;
	psl->size++;
}

//顺序表 判断扩容
void SeqListCheckCapacity(SL* psl)
{
	if (psl->size == psl->capacity)//判断空间是否足够
	{
		//扩容
		SLDateType newcapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
		SLDateType* tmp = (SLDateType*)realloc(psl->array, newcapacity * sizeof(SLDateType));;
		if (tmp == NULL)
		{
			printf("扩容失败!\n");
			exit(-1);
		}

		//扩容后需要更新的
		psl->array = tmp;
		psl->capacity = newcapacity;
	}
}

//顺序表 头删
void SeqListPopFront(SL* psl)
{
	//assert(psl->size > 0);
	//int begin = 1;
	//while (beginsize)
	//{
	//	psl->array[begin - 1] = psl->array[begin];
	//	begin++;
	//}
	//psl->size--;

	//复用SeqListEarse
	SeqListEarse(psl, 0);
}

//顺序表 查找
int SeqListFind(SL* psl,SLDateType x)
{
	//数组下标从0开始
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		if (psl->array[i] == x)
		{
			return i;
		}
	}

	//没找到
	return -1;
}

//顺序表 任意pos位置插入
int SeqListInsert(SL* psl, int pos, SLDateType x)
{
	//插入有区域约束

	//温柔处理:
	/*	if (pos < 0 && pos > psl->size)
	{
		printf("pos invalidd(pos非法!)\n");
		return;
	}*/

	//暴力解决
	assert(pos >= 0 && pos <= psl->size);

	//凡是插入,考虑是否需要扩容
	SeqListCheckCapacity(psl);

	//挪数据
	int end = psl->size - 1;
	while (end>=pos)
	{
		psl->array[end + 1] = psl->array[end];
		end--;
	}

	//正式插入
	psl->array[pos] = x;
	psl->size++;
}

//顺序表 任意pos位置删除
void SeqListEarse(SL* psl, int pos)
{
	//删除和插入一样:有区域约束,不可任意访问删除
	  
	//温柔处理
	/*	if (pos < 0 && pos >= 0)
	{
		printf("pos invalidd(pos非法!)\n");
		return;
	}*/

	//暴力处理
	assert(pos >= 0 && pos < psl->size);

	int begin = pos;
	while (begin < psl->size)
	{
		psl->array[begin] = psl->array[begin + 1];
		begin++;
	}

	psl->size--;
}

Test.c

#include"SeqList.h"

void TestSeqList1()
{
	SL sl;
	SeqListInit(&sl);
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	SeqListPushBack(&sl, 5);
	SeqListPrint(&sl);

	//SeqListPopBack(&sl);
	//SeqListPopBack(&sl);
	//SeqListPopBack(&sl);
	//SeqListPopBack(&sl);
	//SeqListPopBack(&sl);
	//SeqListPopBack(&sl);
	//SeqListPrint(&sl);

	//SeqListPushBack(&sl, 10);
	//SeqListPushBack(&sl, 20);
	//SeqListPrint(&sl);

	//SeqListPushFront(&sl, 1);
	//SeqListPushFront(&sl, 2);
	//SeqListPushFront(&sl, 3);
	//SeqListPushFront(&sl, 4);

	//SeqListPrint(&sl);

	//SeqListDestory(&sl);
}

void TestSeqList2()
{
	SL sl;
	SeqListInit(&sl);

	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);

	SeqListPrint(&sl);

	SeqListPushFront(&sl, 1);
	SeqListPushFront(&sl, 2);
	SeqListPushFront(&sl, 3);
	SeqListPushFront(&sl, 4);
	SeqListPrint(&sl);

	//SeqListDestory(&sl);
}

void TestSeqList3()
{
	SL sl;
	SeqListInit(&sl);
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);


	SeqListPrint(&sl);

	//int ret = SeqListFind(&sl, 3);
	//if (ret == -1)
	//	printf("找不到!\n");
	//else
	//	printf("找到了,下标为%d", ret);

	SeqListInsert(&sl, 5, 5);
	SeqListPrint(&sl);
	SeqListDestory(&sl);
}

void TestSeqList4()
{
	SL sl;
	SeqListInit(&sl);
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	SeqListPushFront(&sl, 1);
	SeqListPushFront(&sl, 2);
	SeqListPushFront(&sl, 3);
	SeqListPushFront(&sl, 4);

	SeqListPrint(&sl);
	SeqListPopFront(&sl);
	SeqListPopBack(&sl);
	SeqListPrint(&sl);

	SeqListDestory(&sl);
}

int main()
{
	//TestSeqList1();
	//TestSeqList2();
	//TestSeqList3();
	TestSeqList4();
}

【极简数据结构】快速了解并实现顺序表,速通玩家的最爱_第6张图片
简单的三连是对作者的最大支持!

✒️ 笔者:陈汉新

更新: 2022.4.13

❌ 勘误: 暂无

声明:由于作者水平有限,本文错误之处在所难免,敬请读者指正!

你可能感兴趣的:(数据结构与算法,数据结构,算法,c语言,c++)