【数据结构(C语言)】顺序表

前言

本篇文章将介绍数据结构中的顺序表以及如何用C语言将其实现

线性表

顺序表是线性表的一种,在学习顺序表之前,我们先来了解一下线性表

什么是线性表?

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使
用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…

线性表,顾名思义,在逻辑上是线性结构,就像一条连续的直线一样;
但是在物理结构上不一定是连续的,可以简单理解为:其中的数据不一定是连续存储的

线性表在物理上存储时,通常是以数组或者链式结构的形式存储

image-20231031203641801

image-20231031203706291

顺序表

什么是顺序表?

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般使用数组来存储数据。

由于顺序表有以上特性,所以进行增删操作时,除了尾插尾删,都需要对数组中的数据进行挪动,以保证存储的数据物理地址连续

顺序表一般可分为静态顺序表和动态顺序表

静态顺序表是指定一个数组的大小,使用这个数组存储数据

#define N 7
typedef int SLDataType;

typedef struct SequenceList
{
	SLDataType data[N];
	int size;
}SeqList;

以上代码中,N 就指定了 data 数组的大小,代表数组的总容量
size 表示数组中实际存了多少数据,每存一个数据就会加一

image-20231031205339441

动态顺序表是使用一个动态开辟的数组来存储数据

typedef int SLDataType;

typedef struct SequenceList
{
	SLDataType* data;
	int size;
	int capacity;
}SeqList;

data 是一个指针,指向一个动态开辟的数组
size 表示数组中的实际的数据个数,每次存储数据后会加一
capacity 表示数组的总容量

当 size 等于 capacity 时,数组就会扩容

image-20231031211312814

以上两种顺序表,一般是动态的使用较多,如果我们提前知道要存多少数据,不需要扩容时也可以使用静态顺序表

接下来我们就来学习如何实现一个动态顺序表的增删查改

模块化

为了提高代码可读性,写起来更加方便,我们将代码封装不同的模块

SequenceList.h

定义数据类型、结构体,还有接口的声明

SequenceList.c

各种接口的实现

test.c

用来测试程序

定义数据类型和结构体

一般我们会将要存储的数据类型重定义为一个新的类型,之后就使用这个新类型进行数据存储,这样当我们想要修改存储的数据类型时,只需要修改一次

例如,将 int 定义为,SLDataType,之后就用 SLDataType 来代替 int
如果以后想存储 char 类型,只需将 int 修改为 char

typedef int SLDataType;

定义结构体

typedef struct SequenceList
{
	SLDataType* data;
	int size;
	int capacity;
}SeqList;

上文已经说明各个变量的作用,这里不再赘述

接口实现

要实现顺序表的增删查改,接需要用到不同的接口

以下是我们要实现的接口

//初始化顺序表
void SeqListInit(SeqList* psl);
//销毁顺序表
void SeqListDestory(SeqList* psl);
//打印顺序表
void SeqListPrint(SeqList* psl);
//检查容量,如果满了就扩容
void CheckCapacity(SeqList* psl);
//尾插
void SeqListPushBack(SeqList* psl, SLDataType val);
//尾删
void SeqListPopBack(SeqList* psl);
//头插
void SeqListPushFront(SeqList* psl, SLDataType val);
//头删
void SeqListPopFront(SeqList* psl);
//查找某个数据,返回下标
int SeqListFind(SeqList* psl, SLDataType val);
//在pos位置插入数据
void SeqListInsert(SeqList* psl, int pos);
//删除pos位置的数据
void SeqListErase(SeqList* psl, int pos);

下面我们依次实现这些接口

初始化顺序表

接收一个结构体指针 psl ,这个结构体就是顺序表
既然我们要对顺序表进行操作,这个顺序表总得存在吧,所以 psl 不能是空指针
所以要进行断言 assert(psl != NULL),后面的接口同理,不再赘述

通过指针 psl 访问结构体内的变量:将 data 置为空指针,size 和 capacity 置为0

void SeqListInit(SeqList* psl)
{
	assert(psl != NULL);

	psl->data = NULL;
	psl->size = psl->capacity = 0;
}

销毁顺序表

既然我们开辟了内存,那程序结束时我们就要及时释放

void SeqListDestory(SeqList* psl)
{
	assert(psl != NULL);

	free(psl->data);
	psl->data = NULL;
}

打印顺序表

遍历顺序表,进行打印

由于每次插入数据后,size 都会 ++,所以 size 表示数组数据个数的同时,也表示最后一个数据的下一个位置

image-20231031221424917

用 i 遍历,i 小于 size ,取不到 size
再用 data 配合 i 访问数据,这里 data 是指向数组的,我们可以将其理解为数组首元素地址来使用

void SeqListPrint(SeqList* psl)
{
	assert(psl != NULL);

	for (int i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->data[i]);
	}
	printf("\n");
}

检查容量

如果 size 和 capacity 相等,说明数组满了,需要进行扩容

在相等的情况下,如果 capacity 为0,说明数组为空,那就给数组一个初始大小
capacity 不为0,那就是数组满了,需要扩容

扩容多大没有规定,看我们需求,一般是扩容为原来的 2 倍

在扩容时,我们使用 realloc ,当数组不为空时,正常执行扩容
当数组为空时,那么 data 指针就是空指针,如果 realloc 收到的是空指针,那么它就会当做 malloc 来使用
image-20231031224213023

void CheckCapacity(SeqList* psl)
{
	assert(psl != NULL);

	if (psl->size == psl->capacity)
	{
		int newcapacity = 0; // 新容量大小
		// 顺序表为空
		if (psl->capacity == 0)
		{
			newcapacity = 4; // 给4个初始大小
		}
		// 不为空
		else
		{
			newcapacity = psl->capacity * 2;
		}
		// 临时变量接收开辟的空间
		SLDataType* tmp = (SLDataType*)realloc(psl->data, newcapacity * sizeof(SLDataType));
		// 如果开辟失败
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		//开辟成功
		psl->data = tmp;
		psl->capacity = newcapacity;
	}
}

尾插

尾插较为简单,直接将数据插入到 size 的位置,size++

注意:插入之前,检查数组容量

image-20231031225218329

void SeqListPushBack(SeqList* psl, SLDataType val)
{
	assert(psl != NULL);

	CheckCapacity(psl);
	psl->data[psl->size] = val;
	psl->size++;
}

测试一下

void test1()
{
	SeqList sl;
	SeqListInit(&sl);
	//尾插
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	//打印
	SeqListPrint(&sl);
    //销毁
    SeqListDestory(&sl);
}
int main()
{
	test1();
	return 0;
}

运行结果:image-20231031225924611

尾删

尾删只需将 size 减一就行,数据不用管,如果插入新数据,尾上的数据会被覆盖

但是注意,size 不能等于 0,也就是顺序表不能为空,如果 size 为0,再减一就成负数了,这是非法的

image-20231031230924627

void SeqListPopBack(SeqList* psl)
{
	assert(psl != NULL);
	assert(psl->size > 0);

	psl->size--;
}

测试

void test1()
{
	SeqList sl;
	SeqListInit(&sl);
	//尾插
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	//打印
	SeqListPrint(&sl);
	//尾删
	SeqListPopBack(&sl);
	SeqListPrint(&sl);
	SeqListPopBack(&sl);
	SeqListPrint(&sl);
	SeqListPopBack(&sl);
	SeqListPrint(&sl);
	SeqListPopBack(&sl);
	SeqListPrint(&sl);
	//销毁
    SeqListDestory(&sl);
}
int main()
{
	test1();
	return 0;
}

运行结果:
image-20231031231307359

那我们再加一条尾删呢?

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

报错,而且会给出报错的行数,这就是断言的好处image-20231031231442312

头插

因为顺序表的特性:存储的数据的物理地址连续
所以头插需要将数据整体向后挪动,为头插的数据让出空位
image-20231101103850097

如何实现:

挪动:定义变量 end = size -1 ,用来指向最后一个数据,从后向前遍历,每次遍历都会将当前位置的数据放到下一个位置,当完成对数组第一个数据的移动后,遍历结束
image-20231101104839506

挪动完成后,插入数据,size++

void SeqListPushFront(SeqList* psl, SLDataType val)
{
	assert(psl != NULL);
	//检查容量
	CheckCapacity(psl);
	//挪动
	int end = psl->size - 1;
	while (end >= 0)
	{
		psl->data[end + 1] = psl->data[end];
		end--;
	}
	//插入数据
	psl->data[0] = val;
	psl->size++;
}

测试

void test2()
{
	SeqList sl;
	SeqListInit(&sl);

	//头插
	SeqListPushFront(&sl, 10);
	SeqListPrint(&sl);
	SeqListPushFront(&sl, 20);
	SeqListPrint(&sl);
	SeqListPushFront(&sl, 30);
	SeqListPrint(&sl);
	SeqListPushFront(&sl, 40);
	SeqListPrint(&sl);
    //销毁
    SeqListDestory(&sl);
}
int main()
{
	test2();
	return 0;
}

运行结果:image-20231101110355338

头删

头删的话,不需要特意去删除第一个数据,只需要将后面的数据整体向前挪动,将要删除的数据覆盖即可

挪动完成,size减一

注意:和尾删一样,顺序表不能为空
image-20231101174854392

如何实现:

定义变量 begin = 1,从前向后进行遍历,begin < size,即完成最后一个数据的挪动后,遍历停止
每次遍历会将当前位置数据挪动到前一个位置
image-20231101111707065

void SeqListPopFront(SeqList* psl)
{
	assert(psl != NULL);
	assert(psl->size > 0);

	int begin = 1;
	while (begin < psl->size)
	{
		psl->data[begin - 1] = psl->data[begin];
		begin++;
	}
	//头删完成后,不要忘记对size减一
	psl->size--;
}

测试

void test2()
{
	SeqList sl;
	SeqListInit(&sl);

	//头插
	SeqListPushFront(&sl, 10);
	SeqListPrint(&sl);
	SeqListPushFront(&sl, 20);
	SeqListPrint(&sl);
	SeqListPushFront(&sl, 30);
	SeqListPrint(&sl);
	SeqListPushFront(&sl, 40);
	SeqListPrint(&sl);
	//头删
	SeqListPopFront(&sl);
	SeqListPrint(&sl);
	SeqListPopFront(&sl);
	SeqListPrint(&sl);
	SeqListPopFront(&sl);
	SeqListPrint(&sl);
	SeqListPopFront(&sl);
	SeqListPrint(&sl);
    //销毁
    SeqListDestory(&sl);
}
int main()
{
	test2();
	return 0;
}

运行结果:
image-20231101112341226

再加一条头删,测试断言:

	//头删
	SeqListPopFront(&sl);
	SeqListPrint(&sl);
	SeqListPopFront(&sl);
	SeqListPrint(&sl);
	SeqListPopFront(&sl);
	SeqListPrint(&sl);
	SeqListPopFront(&sl);
	SeqListPrint(&sl);
	SeqListPopFront(&sl);

正常报错:
image-20231101112627317

查找某个数据

遍历查找,找到相应数据就返回下标,找不到就返回 -1

int SeqListFind(SeqList* psl, SLDataType val)
{
	assert(psl != NULL);

	for (int i = 0; i < psl->size; i++)
	{
		if (psl->data[i] == val)
		{
			return i;
		}
	}
	//找不到
	return -1;
}

测试

void test3()
{
	SeqList sl;
	SeqListInit(&sl);

	//插入数据
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	//查找3
	int pos3 = SeqListFind(&sl, 3);
	printf("pos3 = %d\n", pos3);
	//查找5
	int pos5 = SeqListFind(&sl, 5);
	printf("pos5 = %d\n", pos5);
    //销毁
    SeqListDestory(&sl);
}
int main()
{
	test3();
	return 0;
}

运行结果:
image-20231101113912633

在 pos 位置插入数据

与头插的原理相同,可以把 pos 位置看作头,然后套用头插的步骤

挪动数据:
定义变量 end = size - 1,从后向前遍历,end >= pos
每次遍历都将当前位置的数据放到下一个位置

在pos位置插入数据,size++

image-20231101162917055

关于 pos 的合法性问题

pos 并不是完全任意位置,在顺序表中是有范围的:0~size

如果,pos大于size,那么插入的数据就不与前面的数据连续,显然是不符合顺序表规定的

image-20231101164910949

void SeqListInsert(SeqList* psl, int pos, SLDataType val)
{
	assert(psl != NULL);
	assert(pos >= 0 && pos <= psl->size);
    //检查容量
	CheckCapacity(psl);
	//挪动数据
	int end = psl->size - 1;
	while (end >= pos)
	{
		psl->data[end + 1] = psl->data[end];
		end--;
	}
	//插入数据
	psl->data[pos] = val;
	psl->size++;
}

测试

void test4()
{
	SeqList sl;
	SeqListInit(&sl);

	//插入数据
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	SeqListPrint(&sl);
	//在pos = 2 位置插入20
	SeqListInsert(&sl, 2, 20);
	SeqListPrint(&sl);
    //销毁
    SeqListDestory(&sl);
}
int main()
{
	test4();
	return 0;
}

运行结果:
image-20231101164115724

删除 pos 位置的数据

同样地,可以将 pos 看作头,接着套用头删的步骤

挪动数据:
定义变量 begin = pos + 1,从前向后遍历,begin < size,不等于 size
每次遍历,将当前位置的数据挪动到前一个位置

挪动完成之后,size减一

image-20231101174542240

pos 的合法范围:0~size,不包括 size

void SeqListErase(SeqList* psl, int pos)
{
	assert(psl != NULL);
	assert(pos >= 0 && pos < psl->size);
	//挪动
	int begin = pos + 1;
	while (begin < psl->size)
	{
		psl->data[begin - 1] = psl->data[begin];
		begin++;
	}
	psl->size--;
}

测试

void test4()
{
	SeqList sl;
	SeqListInit(&sl);

	//插入数据
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	SeqListPrint(&sl);
	//在pos = 2 位置插入20
	SeqListInsert(&sl, 2, 20);
	SeqListPrint(&sl);
	//删除pos = 2位置的数据
	SeqListErase(&sl, 2);
	SeqListPrint(&sl);
    //销毁
    SeqListDestory(&sl);
}
int main()
{
	test4();
	return 0;
}

运行结果:
image-20231101175602775

代码

SequenceList.h

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;

typedef struct SequenceList
{
	SLDataType* data;
	int size;
	int capacity;
}SeqList;

//初始化顺序表
void SeqListInit(SeqList* psl);
//销毁顺序表
void SeqListDestory(SeqList* psl);
//打印顺序表
void SeqListPrint(SeqList* psl);
//检查容量,如果满了就扩容
void CheckCapacity(SeqList* psl);
//尾插
void SeqListPushBack(SeqList* psl, SLDataType val);
//尾删
void SeqListPopBack(SeqList* psl);
//头插
void SeqListPushFront(SeqList* psl, SLDataType val);
//头删
void SeqListPopFront(SeqList* psl);
//查找某个数据,返回下标
int SeqListFind(SeqList* psl, SLDataType val);
//在pos位置插入数据
void SeqListInsert(SeqList* psl, int pos, SLDataType val);
//删除pos位置的数据
void SeqListErase(SeqList* psl, int pos);

SequenceList.c

#include "SequenceList.h"

//初始化顺序表
void SeqListInit(SeqList* psl)
{
	assert(psl != NULL);

	psl->data = NULL;
	psl->size = psl->capacity = 0;
}
//销毁顺序表
void SeqListDestory(SeqList* psl)
{
	assert(psl != NULL);

	free(psl->data);
	psl->data = NULL;
}
//打印顺序表
void SeqListPrint(SeqList* psl)
{
	assert(psl != NULL);

	for (int i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->data[i]);
	}
	printf("\n");
}
//检查容量,如果满了就扩容
void CheckCapacity(SeqList* psl)
{
	assert(psl != NULL);

	if (psl->size == psl->capacity)
	{
		int newcapacity = 0; // 新容量大小
		// 顺序表为空
		if (psl->capacity == 0)
		{
			newcapacity = 4; // 给4个初始大小
		}
		// 不为空
		else
		{
			newcapacity = psl->capacity * 2;
		}
		// 临时变量接收开辟的空间
		SLDataType* tmp = (SLDataType*)realloc(psl->data, newcapacity * sizeof(SLDataType));
		// 如果开辟失败
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}

		psl->data = tmp;
		psl->capacity = newcapacity;
	}
}
//尾插
void SeqListPushBack(SeqList* psl, SLDataType val)
{
	assert(psl != NULL);

	CheckCapacity(psl);
	psl->data[psl->size] = val;
	psl->size++;
}
//尾删
void SeqListPopBack(SeqList* psl)
{
	assert(psl != NULL);
	assert(psl->size > 0);

	psl->size--;
}
//头插
void SeqListPushFront(SeqList* psl, SLDataType val)
{
	assert(psl != NULL);
	//检查容量
	CheckCapacity(psl);
	//挪动
	int end = psl->size - 1;
	while (end >= 0)
	{
		psl->data[end + 1] = psl->data[end];
		end--;
	}
	//插入数据
	psl->data[0] = val;
	psl->size++;
}
//头删
void SeqListPopFront(SeqList* psl)
{
	assert(psl != NULL);
	assert(psl->size > 0);

	int begin = 1;
	while (begin < psl->size)
	{
		psl->data[begin - 1] = psl->data[begin];
		begin++;
	}
	//头删完成后,不要忘记对size减一
	psl->size--;
}
//查找某个数据,返回下标
int SeqListFind(SeqList* psl, SLDataType val)
{
	assert(psl != NULL);

	for (int i = 0; i < psl->size; i++)
	{
		if (psl->data[i] == val)
		{
			return i;
		}
	}
	//找不到
	return -1;
}
//在pos位置插入数据
void SeqListInsert(SeqList* psl, int pos, SLDataType val)
{
	assert(psl != NULL);
	assert(pos >= 0 && pos <= psl->size);
	//检查容量
	CheckCapacity(psl);
	//挪动数据
	int end = psl->size - 1;
	while (end >= pos)
	{
		psl->data[end + 1] = psl->data[end];
		end--;
	}
	//插入数据
	psl->data[pos] = val;
	psl->size++;
}
//删除pos位置的数据
void SeqListErase(SeqList* psl, int pos)
{
	assert(psl != NULL);
	assert(pos >= 0 && pos < psl->size);
	//挪动
	int begin = pos + 1;
	while (begin < psl->size)
	{
		psl->data[begin - 1] = psl->data[begin];
		begin++;
	}
	psl->size--;
}

test.c

#include "SequenceList.h"

void test1()
{
	SeqList sl;
	SeqListInit(&sl);
	//尾插
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	//打印
	SeqListPrint(&sl);
	//尾删
	SeqListPopBack(&sl);
	SeqListPrint(&sl);
	SeqListPopBack(&sl);
	SeqListPrint(&sl);
	SeqListPopBack(&sl);
	SeqListPrint(&sl);
	SeqListPopBack(&sl);
	SeqListPrint(&sl);
	//SeqListPopBack(&sl);

	SeqListDestory(&sl);
}

void test2()
{
	SeqList sl;
	SeqListInit(&sl);

	//头插
	SeqListPushFront(&sl, 10);
	SeqListPrint(&sl);
	SeqListPushFront(&sl, 20);
	SeqListPrint(&sl);
	SeqListPushFront(&sl, 30);
	SeqListPrint(&sl);
	SeqListPushFront(&sl, 40);
	SeqListPrint(&sl);
	//头删
	SeqListPopFront(&sl);
	SeqListPrint(&sl);
	SeqListPopFront(&sl);
	SeqListPrint(&sl);
	SeqListPopFront(&sl);
	SeqListPrint(&sl);
	SeqListPopFront(&sl);
	SeqListPrint(&sl);
	//SeqListPopFront(&sl);

	SeqListDestory(&sl);
}

void test3()
{
	SeqList sl;
	SeqListInit(&sl);

	//插入数据
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	//查找3
	int pos3 = SeqListFind(&sl, 3);
	printf("pos3 = %d\n", pos3);
	//查找5
	int pos5 = SeqListFind(&sl, 5);
	printf("pos5 = %d\n", pos5);

	SeqListDestory(&sl);
}

void test4()
{
	SeqList sl;
	SeqListInit(&sl);

	//插入数据
	SeqListPushBack(&sl, 1);
	SeqListPushBack(&sl, 2);
	SeqListPushBack(&sl, 3);
	SeqListPushBack(&sl, 4);
	SeqListPrint(&sl);
	//在pos = 2 位置插入20
	SeqListInsert(&sl, 2, 20);
	SeqListPrint(&sl);
	//删除pos = 2位置的数据
	SeqListErase(&sl, 2);
	SeqListPrint(&sl);

	SeqListDestory(&sl);
}
int main()
{
	test4();
	return 0;
}

总结

顺序表是一种线性结构,要求物理存储结构连续
尾插、尾删较为方便快速
可以根据下标,实现随机访问
中间/头部的插入删除,需要挪动数据,效率较低
增容需申请空间,拷贝旧数据,释放旧空间,存在一定损耗
增容一般是二倍,可能会造成空间的浪费。比如原空间为1000满了,增容为2000,但是只存1个数据,浪费空间

结束,再见:D

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿洵Rain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值