【数据结构】C语言--顺序表(附完整源码和注释)

文章目录

  • 前言
  • 一、顺序表的结构
  • 二、顺序表增删改查的实现
    • 1.顺序表实现各函数
    • 2.顺序表的初始化(SeqListInit)
    • 3.顺序表的尾插尾删(SeqListPushBack/SeqListPopBack)
      • (1)尾插
      • (2)尾删
    • 4.顺序表的头插头删和扩容(SeqListPushFront/SeqListPopFront/SeqListCheckCapacity)
      • (1)头插
      • (2)头删
    • 5.对顺序表进行打印
    • 6.顺序表指定值位置查找(SeqListFind)
    • 7.顺序表在pos位置插入x/顺序表删除pos位置的值(SeqListInsert/SeqListErase)
      • (1)指定位置插入
      • (2)指定位置删除
    • 8.顺序表的销毁
  • 三、完整源码
    • 1.SeqList.h
    • 2.SeqList.c

前言

顺序表是数据结构学习所接触的第一个数据存储结构,对顺序表的结构有清楚的了解,将对后面的学习大有帮助。(本文章默认读者c语言有一定了解)还需要注意的是:数据结构的学习,我们亲自画图(理解逻辑的过程)十分重要,如果顺序表不好好画图,相信链表的实现时会让你头疼一阵。

一、顺序表的结构

【数据结构】C语言--顺序表(附完整源码和注释)_第1张图片

typedef int SLDateType;
typedef struct SeqList
{
	SLDateType* a;
	int size;
	int capacity;
}SeqList;

顺序表的载体是一个结构体,它实际的数据将储存在指针a所指向的一片空间中
(我们用malloc开辟)。而size和capacity同样作为顺序表的属性成为顺序表结构体中的成员,这样可以方便我们随时获取顺序表的状态。


二、顺序表增删改查的实现

1.顺序表实现各函数

// SeqList.h
#pragma once
#include 
#include 
#include 
#include
#define INIT_CAPACITY 3

typedef int SLDateType;
typedef struct SeqList
{
	SLDateType* a;
	int size;
	int capacity;
}SeqList;

// 对数据的管理:增删查改 
//初始化
void SeqListInit(SeqList* ps);
//销毁
void SeqListDestroy(SeqList* ps);
//打印查看
void SeqListPrint(SeqList* ps);
//尾插
void SeqListPushBack(SeqList* ps, SLDateType x);
//头插
void SeqListPushFront(SeqList* ps, SLDateType x);
//头删
void SeqListPopFront(SeqList* ps);
//尾删
void SeqListPopBack(SeqList* ps);
//检查容量大小/并扩容
void SeqListCheckCapacity(SeqList* ps);

// 顺序表查找
int SeqListFind(SeqList* ps, SLDateType x);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, int pos, SLDateType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, int pos);

2.顺序表的初始化(SeqListInit)

顺序表结构体中的成员a指针在结构体变量刚被创建时是一个野指针(我们没有给予初始化,它是随机的,也无法得知该指针指向哪里),如果这时对该指针进行解引用,就会非法访问内存空间。
所以我们用malloc函数对成员指针a进行空间开辟(在堆上),malloc开辟的空间将在内存中具有连续性(和数组一样),也可解引用找到顺序表的各个数据。
【数据结构】C语言--顺序表(附完整源码和注释)_第2张图片

错误代码示例

void SeqListInit(SeqList* ps)
{
	ps->capacity = INIT_CAPACITY;
	for (int i = 0; i < ps->capacity; i++)
	{
		*(ps->a + i)=0;
	}
	ps->size = 0;
}

在这里插入图片描述
这是新手常见的初始化错误,对野指针解引用!
正确的初始化(如下)

void SeqListInit(SeqList* ps)
{
	assert(ps);
	//开辟空间
	ps->a = (SLDateType*)malloc(sizeof(SLDateType) * INIT_CAPACITY);
	//开辟失败报错
	if (ps->a == NULL)
	{
		perror("malloc fail");
		return;
	}
	ps->size = 0;
	ps->capacity = INIT_CAPACITY;
}

这里注意的是:1.对参数ps进行断言(我们无法对空的结构体指针进行成员访问)
2.当malloc开辟失败时我们打印错误信息并结束该函数。(一般不会失败)
(后面的数据结构学习我将逐渐减少强调)

3.顺序表的尾插尾删(SeqListPushBack/SeqListPopBack)

(1)尾插

尾插时结构体中size的大小刚好是a指向的下一个位置下标的位置,且尾插不需要移动数组中其他值的位置,所以尾插是顺序表中比较推荐的数据插入方式。
【数据结构】C语言--顺序表(附完整源码和注释)_第3张图片
值得注意的是:当size的大小等于capacity时,我们需要对顺序表进行扩容操作!!(我们选择给顺序表扩容2倍)

void SeqListPushBack(SeqList* ps, SLDateType x)
{
	assert(ps);
	//扩容
	if (ps->size == ps->capacity)
	{
		SLDateType* tmp = (SLDateType*)realloc(ps->a,sizeof(SLDateType) * 2 * ps->capacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		else
		{
			ps->a = tmp;
		}
		ps->capacity *= 2;
	}
	 //SeqListCheckCapacity(ps);
	//开始尾插
	ps->a[ps->size] = x;
	ps->size++;

对已经申请的内存空间扩容时,要使用realloc函数,它两个参数分别是原地址和希望扩容的内存大小(单位是也是字节), 注意realloc也有可能开辟失败(当需要的内存太大时),所以要做好报错准备,因为它需要保持像数组那样的顺序连贯的结构,所以当其指针所指空间的后面内存空间不足时,它会另寻足够空间的位置开辟,也就是所谓的异地扩容,此时realloc返回的指向开辟空间的指针的位置改变,需要另外一个临时指针变量做过渡。

(2)尾删

尾删就更简单了,我们只需要对size进行减一,无需担心没有删除已存的值,新的值插入时会将它替换。
【数据结构】C语言--顺序表(附完整源码和注释)_第4张图片
不过空的顺序表无法删除,我们最好断言它,同时不要忘记对函数的参数指针进行断言。

void SeqListPopBack(SeqList* ps)
{
	assert(ps);
	assert(ps->size > 0);
//	if (ps->size == 0)
//	{
//		return;
//	}
	ps->a[ps->size - 1] = 0;
	ps->size--;
}
//虽然注释里的报错也可以,但是最好使用暴力一点的报错assert。

4.顺序表的头插头删和扩容(SeqListPushFront/SeqListPopFront/SeqListCheckCapacity)

(1)头插

在顺序表里我们头插头删时,最差需要调整表中每个值的位置,所以时间复杂度是O(N),不高效。

【数据结构】C语言--顺序表(附完整源码和注释)_第5张图片

void SeqListPushFront(SeqList* ps, SLDateType x)
{
	assert(ps);
	//扩容
	SeqListCheckCapacity(ps);
	int end=ps->size-1;
	while (end >= 0)
	{
		ps->a[end + 1] = ps->a[end];
		end--;
	}
	ps->a[0] = x;
	ps->size++;
}

注意上动图的size已经等于capacity了,所以在下次进入插入相关的函数时,我们需要对顺序表进行扩容,既然已经或者将要扩容多次,那么不如将扩容操作封装成一个函数 SeqListCheckCapacity。

void SeqListCheckCapacity(SeqList* ps)
{
	assert(ps);
	if (ps->size == ps->capacity)
	{
		SLDateType* tmp = (SLDateType*)realloc(ps->a, sizeof(SLDateType) * 2 * ps->capacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
			ps->a = tmp;
		
		ps->capacity *= 2;
		printf("扩容成功!");
	}

}

(2)头删

void SeqListPopFront(SeqList* ps)
{
	assert(ps);
	assert(ps->size > 0);
	for (int i = 0; i < ps->size-1; i++)
	{
		ps->a[i] = ps->a[i + 1];
	}
	ps->size--;
}

5.对顺序表进行打印

其实对于一个程序员而言,写一个程序,自己代码占3成,另外7成靠copy,,,,,,,,
开玩笑的O(∩_∩)O~,另外7成是调试,有时候虽然我们的代码能跑(没有报错),但是储存效果却不尽人意,所以数据结构学习中,我们最方便的调试方法就是在各函数正常运作的情况下尽快的、正确的写出一个打印函数(如果你不会调试的话),以便我们检查结构中的储存情况。

void SeqListPrint(SeqList* ps)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->a[i]);
	}
	printf("\n");
}

->操作符和[]的优先级一致,所以遵循从左到右的计算顺序。
注意:这里的参数结构体指针ps不能为空(不能对空的结构体指针进行成员访问),但是ps成员中的指针a可以为空(空顺序表也可以打印),这里看着简单,可是请你格外记忆理解,不要对它嗤之以鼻,如果不想被链表的断言扇巴掌的话。

6.顺序表指定值位置查找(SeqListFind)

根据参数值查找顺序表中对应值得位置并返回。
【数据结构】C语言--顺序表(附完整源码和注释)_第6张图片

int SeqListFind(SeqList* ps, SLDateType x)
{
	assert(ps);
	int pos=-1;
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->a[i] == x)
		{
			pos = i;
			return pos;
		}
	}
	return pos;
}

7.顺序表在pos位置插入x/顺序表删除pos位置的值(SeqListInsert/SeqListErase)

这里的指定位置的插入删除和头插头删逻辑差不多,而且不知道你们有没有发现 ,其实只要完成了对指定位置pos位置的插入和删除的函数,我们的所有插入删除函数都可以用这两个函数实现(头插不就是指定下标为0的位置插入吗?尾删不就是指定下标为size-1的位置删除吗?)这样我们不就可以轻松解锁成就:15分钟写完一个顺序表~~~了吗。O(∩_∩)O

(1)指定位置插入

void SeqListInsert(SeqList* ps, int pos, SLDateType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size - 1);
	SeqListCheckCapacity(ps);
	for (int i = ps->size - 1; i >= pos; i--)
	{
		ps->a[i + 1] = ps->a[i];
	}
	ps->size++;
	ps->a[pos] = x;
}

(2)指定位置删除

void SeqListErase(SeqList* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size - 1);
	for (int i = pos; i <= ps->size - 1; i++)
	{
		ps->a[i] = ps->a[i + 1];
	}
	ps->size--;
}

8.顺序表的销毁

销毁也是代码新手容易常错的地方。
错误代码如下

void SeqListDestroy(SeqList* ps)
{
	free(ps);
	free(ps->a);
	ps->size = 0;
	ps->capacity = 0;
	ps=NULL}

错误:1.释放了结构体指针所指向的位置却又访问它的内存(非法访问)。
2.释放了成员a的内存却不置空(容易再次访问a,可是内存已经释放,所以依旧是非法访问)
(虽然有了错误1错误2就不会发生)
3.对函数中的形式参数进行改值(我们无法在函数中改变一个形式参数的值,除非传它的指针给函数,解引用访问,这里就是传指针的指针<二级指针>)。

正确代码如下

void SeqListDestroy(SeqList* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->capacity = ps->size = 0;
}

正如错误3中所言,我们只能将结构体指针的置空交给函数的使用者!(也就是进行对实参的置空)
注意:1.断言ps不为空指针
2.断言pos的位置为顺序表中有效位置

三、完整源码

1.SeqList.h

#define _CRT_SECURE_NO_WARNINGS 1

// SeqList.h
#pragma once
#include 
#include 
#include 
#include
#define INIT_CAPACITY 3

typedef int SLDateType;
typedef struct SeqList
{
	SLDateType* a;
	int size;
	int capacity;
}SeqList;

// 对数据的管理:增删查改 
void SeqListInit(SeqList* ps);
void SeqListDestroy(SeqList* ps);
void SeqListPrint(SeqList* ps);
void SeqListPushBack(SeqList* ps, SLDateType x);
void SeqListPushFront(SeqList* ps, SLDateType x);
void SeqListPopFront(SeqList* ps);
void SeqListPopBack(SeqList* ps);
void SeqListCheckCapacity(SeqList* ps);

// 顺序表查找
int SeqListFind(SeqList* ps, SLDateType x);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, int pos, SLDateType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, int pos);

2.SeqList.c

//标准初始化
void SeqListInit(SeqList* ps)
{
	ps->a = (SLDateType*)malloc(sizeof(SLDateType) * INIT_CAPACITY);
	if (ps->a == NULL)
	{
		perror("malloc fail");
		return;
	}
	ps->size = 0;
	ps->capacity = INIT_CAPACITY;
}
//顺序表结构体里,SLDateType*只是一个指针,为它在内存堆区里自定义开辟内存区域(使其自定义指向想要的内存大小)。

//标准顺序表销毁
void SeqListDestroy(SeqList* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->capacity = ps->size = 0;
}
//对malloc开辟(借用)的内存空间用完记得释放(还给操作系统)
//而对指向该空间的指针记得置空,以防意外非法访问已经释放的空间。


//标准顺序表打印
void SeqListPrint(SeqList* ps)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->a[i]);
	}
	printf("\n");
}
//->操作符和[]的优先级一致,所以遵循从左到右的计算顺序。

//标准顺序表尾插
void SeqListPushBack(SeqList* ps, SLDateType x)
{
	assert(ps);
	扩容
	//if (ps->size == ps->capacity)
	//{
	//	SLDateType* tmp = (SLDateType*)realloc(ps->a,sizeof(SLDateType) * 2 * ps->capacity);
	//	if (tmp == NULL)
	//	{
	//		perror("realloc fail");
	//		return;
	//	}
	//	else
	//	{
	//		ps->a = tmp;
	//	}
	//}
	 SeqListCheckCapacity(ps);
	//开始尾插
	ps->a[ps->size] = x;
	ps->size++;
}
//对已经申请的内存空间扩容时,要使用realloc函数,它两个参数分别是原地址和希望扩容的内存大小(单位是字节),
// 注意realloc也有可能开辟失败(当需要的内存太大时),所以要做好报错准备
//因为它需要保持像数组那样的顺序连贯的结构,所以当其指针所指空间的后面内存空间不足时,它会另寻
//足够空间的位置开辟,也就是所谓的异地扩容,此时指针的位置改变,需要另外一个临时指针变量做过渡。



//标准顺序表尾删
void SeqListPopBack(SeqList* ps)
{
	assert(ps->size > 0);
//	if (ps->size == 0)
//	{
//		return;
//	}
	ps->a[ps->size - 1] = 0;
	ps->size--;
}
//虽然注释里的报错也可以,但是最好使用暴力一点的报错assert,
//它会使程序直接弹出报错窗口,更容易找到程序的错误所在。

// 标准的顺序表头插
//头插头删在顺序表里时间复杂度是O(N),不高效。
void SeqListPushFront(SeqList* ps, SLDateType x)
{
	assert(ps);
	//扩容
	SeqListCheckCapacity(ps);
	int end=ps->size-1;
	while (end >= 0)
	{
		ps->a[end + 1] = ps->a[end];
		end--;
	}
	ps->a[0] = x;
	ps->size++;
}
//头插也需要扩容,既然已经或者将要扩容多次,那么不如将扩容操作封装成一个函数SeqListCheckCapacity。


void SeqListCheckCapacity(SeqList* ps)
{
	assert(ps);
	if (ps->size == ps->capacity)
	{
		SLDateType* tmp = (SLDateType*)realloc(ps->a, sizeof(SLDateType) * 2 * ps->capacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
			ps->a = tmp;
		
		ps->capacity *= 2;
		printf("扩容成功!");
	}

}

//标准的顺序表头删
void SeqListPopFront(SeqList* ps)
{
	assert(ps);
	assert(ps->size > 0);
	for (int i = 0; i < ps->size-1; i++)
	{
		ps->a[i] = ps->a[i + 1];
	}
	ps->size--;
}


//标准顺序表查找
int SeqListFind(SeqList* ps, SLDateType x)
{
	assert(ps);
	int pos=-1;
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->a[i] == x)
		{
			pos = i;
			return pos;
		}
	}
	return pos;
}


//标准顺序表指定位置插入
void SeqListInsert(SeqList* ps, int pos, SLDateType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size - 1);
	SeqListCheckCapacity(ps);
	for (int i = ps->size - 1; i >= pos; i--)
	{
		ps->a[i + 1] = ps->a[i];
	}
	ps->size++;
	ps->a[pos] = x;
}

//标准的顺序表指定位置删除
void SeqListErase(SeqList* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size - 1);
	for (int i = pos; i <= ps->size - 1; i++)
	{
		ps->a[i] = ps->a[i + 1];
	}
	ps->size--;
}

本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点:)。

你可能感兴趣的:(数据结构,c语言,学习,链表)