一篇带你走进线性表之顺序表(C语言阐述)——逐行解释代码

在这里插入图片描述

1. 引言

- 顺序表在数据结构中的地位和作用

顺序表是一种线性表的实现方式,通过数组来存储元素,具有快速的随机访问特性。顺序表的设计使得元素在内存中连续存储,这样就可以通过下标直接访问元素,而无需遍历整个表格。这种特性使得顺序表在需要频繁访问元素和对内存空间要求严格的场景下具有显著的优势。在算法设计和实际编程中,顺序表常常被广泛应用,对于提高程序执行效率和节约内存空间都具有重要意义。

- 概述本文将讨论的内容和结构

2. 顺序表的基本概念

- 定义:什么是顺序表?

顺序表是一种线性表的存储结构,它通过一段连续的内存空间存储数据元素,元素之间的逻辑关系与物理位置一一对应。顺序表可以用数组实现,也被称为数组表。在顺序表中,元素的存储是按照其在逻辑上的顺序依次存放的,通过元素在数组中的索引来定位和访问。

- 结构:顺序表的内部结构和特点

顺序表的内部结构由两部分组成:数据存储区和控制信息区。数据存储区用于存放数据元素,通常采用数组来实现;控制信息区用于存储顺序表的一些信息,如表的长度、容量等。

顺序表的特点包括

  • 元素的逻辑次序与物理次序一致;
  • 内存空间是连续分配的;
  • 具有固定的大小(预分配的空间大小);
  • 可以通过下标直接访问元素;
  • 插入和删除操作可能需要移动其他元素。

3. 实现一个基本的顺序表

在这里我们要实现顺序表的初始化、销毁、打印、检查剩余容量+扩容、尾插、头插、尾删、头删、任意下标位置的插入、任意下标位置的删除、查找、二分查找。

需要用到的头文件

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

定义顺序表的基本结构和属性

typedef int SLDataType;		//方便我们随时更改顺序表的数类型
typedef struct SequenceList
{
	SLDataType* a;	// a是指向 SLDataType 类型的指针,用于存储实际的数据数组
	int size;		// 用于表示当前顺序表中有效数据的个数
	int capacity;	// 用于表示当前顺序表的空间容量
};

SLDataType* a:这是一个指向 SLDataType 类型的指针,用于存储实际的数据数组。在顺序表中,通过这个指针可以访问顺序表中的各个元素。

int size:size 用于表示当前顺序表中有效数据的个数。在对顺序表进行操作时,size
可以帮助确定当前顺序表中有多少个元素是有效的。

int capacity:capacity 用于表示当前顺序表的空间容量,即可以容纳的元素个数上限。当顺序表中的元素数量接近
capacity 时,我们需要进行动态扩容操作。

实现顺序表的初始化

void SLInit(SL* s1)
{
	assert(s1);		//确保传入的参数 s1 不为空。如果 s1 为空,程序会中止执行并输出错误信息
	s1->a = NULL;	
	s1->size = 0;
	s1->capacity = 0;
}

assert(s1):使用 assert 宏确保传入的参数 s1 不为空。如果 s1 为空,assert 会触发断言失败,程序会中止执行并输出错误信息。

s1->a = NULL;:将 s1 指向的顺序表的数据数组指针设置为 NULL,表示暂时没有分配内存来存储数据。

s1->size = 0;:将 s1 指向的顺序表的有效数据个数设置为 0,表示当前顺序表中没有有效数据。

s1->capacity = 0;:将 s1 指向的顺序表的空间容量设置为 0,表示当前顺序表的容量为 0。

销毁

void SLDestroy(SL* s1)
{
	assert(s1);
	if (s1->a != NULL)
	{
		free(s1->a);
		s1->a = NULL;
		s1->size = 0;
		s1->capacity = 0;
	}
}

if (s1->a != NULL):检查顺序表的数据数组指针是否为空,避免对空指针进行操作。

free(s1->a):释放顺序表的数据数组所占用的内存空间。

s1->a = NULL;:将顺序表的数据数组指针设置为空,避免出现野指针。

s1->size = 0;:将顺序表的有效数据个数设置为 0,表示当前顺序表中没有有效数据。

s1->capacity = 0;:将顺序表的空间容量设置为 0,表示当前顺序表的容量为 0。

打印

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

for (int i = 0; i < s1->size; i++):使用循环遍历顺序表中的所有有效数据。

printf(“%d”, s1->a[i]):对于每个有效数据,使用 printf 函数打印它的值。

printf(“\n”):在循环结束后,使用 printf 函数打印一个换行符,以便下次输出不会与当前输出混在一起。

检查容量

void SLCheckCapacity(SL* sl)
{
	assert(sl);
	if (sl->size == sl->capacity)
	{
		int newCapacity = sl->capacity == 0 ? 4 : sl->capacity * 2;
		SLDataType* tmp = (SLDataType*)realloc(sl->a, sizeof(SLDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		sl->a = tmp;
		sl->capacity = newCapacity;
	}
}

----assert(sl):同样使用了 assert 宏,用于确保传入的参数 sl 不为空。如果 sl 为空,assert
会触发断言失败,程序会中止执行并输出错误信息。

----if (sl->size ==sl->capacity):判断当前顺序表的有效数据个数(size)是否等于容量(capacity)。如果相等,表示当前顺序表已经满了,需要进行动态扩容操作。

----int newCapacity = sl->capacity == 0 ? 4 : sl->capacity *2;:根据当前顺序表的容量计算新的容量。如果原容量为 0,则将新容量设置为 4;否则将新容量设置为原容量的两倍。

----SLDataType* tmp = (SLDataType*)realloc(sl->a, sizeof(SLDataType) *newCapacity);:使用 realloc 函数重新分配内存给顺序表的数据数组。将原来的数据数组指针 sl->a按照新容量分配的大小进行重新分配内存,并将返回的指针赋值给临时变量 tmp。

----if (tmp == NULL):检查 realloc 函数是否成功分配了新的内存。如果分配失败,tmp 会为 NULL。

----perror(“realloc fail”):如果 realloc 函数分配失败,使用 perror 函数输出错误信息。

----- sl->a = tmp;:将重新分配的内存指针 tmp 赋值给顺序表的数据数组指针 sl->a。

----sl->capacity = newCapacity;:将顺序表的容量设置为新容量。

尾插

void SLPushBack(SL* sl,SLDataType k)
{
	assert(sl);
	SLCheckCapacity(sl);
	sl->a[sl->size] = k;
	sl->size++;
}

SLCheckCapacity(sl):调用 SLCheckCapacity 函数,用于检查顺序表的容量是否足够,如果不足则进行扩容操作。

sl->a[sl->size ] = k:将新的数据 k 赋值给顺序表的最后一个位置(下标为 sl->size )。

sl->size++:将顺序表的有效数据个数加一,表示插入了一个新的数据。

头插

void SLPushFront(SL* psl, SLDataType x)
{
	assert(psl);
	SLCheckCapacity(psl);
	int end = psl->size - 1;
	while (end >= 0)
	{
		psl->a[end + 1] = psl->a[end];
		--end;
	}
	psl->a[0] = x;
	psl->size++;
}

SLCheckCapacity(sl):调用 SLCheckCapacity 函数,用于检查顺序表的容量是否足够,如果不足则进行扩容操作。

for (int i = sl->size-1; i > 0; i–):使用循环从顺序表的最后一个元素开始,向前遍历到第一个元素。

sl->a[i + 1] = sl->a[i]:将当前位置的数据向后移动一位,为新的数据腾出空间。

sl->a[0] = k:将新的数据 k 插入到顺序表的开头位置(下标为 0)。

sl->size++:将顺序表的有效数据个数加一,表示插入了一个新的数据。

尾删

void SLPopBack(SL* sl)
{
	assert(sl);
	assert(sl->size > 0);
	sl->size--;
}

assert(sl):使用 assert 宏确保传入的参数 sl 不为空。如果 sl 为空,assert
会触发断言失败,程序会中止执行并输出错误信息。

assert(sl->size > 0):使用 assert
宏确保顺序表中至少有一个数据可供删除。如果顺序表中没有数据可供删除,assert 会触发断言失败,程序会中止执行并输出错误信息。

sl->size–:将顺序表的有效数据个数减一,表示删除了最后一个数据。

头删

void SLPopFront(SL* sl)
{
	assert(sl);
	assert(sl->size > 0);
	for (int i = 1; i < sl->size; i++)
	{
		sl->a[i-1] = sl->a[i];
	}
	sl->size--;
}

-----assert(sl):使用 assert 宏确保传入的参数 sl 不为空。如果 sl 为空,assert会触发断言失败,程序会中止执行并输出错误信息。

----assert(sl->size > 0):使用 assert宏确保顺序表中至少有一个数据可供删除。如果顺序表中没有数据可供删除,assert 会触发断言失败,程序会中止执行并输出错误信息。

for (int i = 1; i < sl->size; i++):使用循环从顺序表的第二个元素开始,向后遍历到最后一个元素。

sl->a[i-1] = sl->a[i]:将当前位置的数据向前移动一位,覆盖前一个位置的数据。

sl->size–:将顺序表的有效数据个数减一,表示删除了第一个数据。

任意下标位置的插入

void SLInsertAnyWhere(SL* sl,int pos,SLDataType k)
{
	assert(sl);
	assert(pos >= 0 && pos <= sl->size);
	for (int i = sl->size - 1; i >= pos; i--)
	{
		sl->a[i + 1] = sl->a[i];
	}
	sl->a[pos] = k;
}

assert(sl):使用 assert 宏确保传入的参数 sl 不为空。如果 sl 为空,assert 会触发断言失败,程序会中止执行并输出错误信息。

assert(pos >= 0 && pos <= sl->size):使用 assert 宏确保插入的位置 pos 合法。如果插入的位置不合法,assert 会触发断言失败,程序会中止执行并输出错误信息。

for (int i = sl->size - 1; i >= pos; i–):使用循环从顺序表的最后一个元素开始,向前遍历到插入位置。

sl->a[i + 1] = sl->a[i]:将当前位置的数据向后移动一位,为新的数据腾出空间。

sl->a[pos] = k:在插入位置处插入新的数据。

sl->size++:将顺序表的有效数据个数加一,表示插入了新的数据。

任意下标位置的删除

void SLPopAnyWhere(SL* sl, int pos, SLDataType k)
{
	assert(sl);
	assert(pos >= 0 && pos <= sl->size);
	for (int i = pos; i < sl->size; i++)
	{
		sl->a[i] = sl->a[i + 1];
	}
	sl->size--;
}

assert(sl):使用 assert 宏确保传入的参数 sl 不为空。如果 sl 为空,assert会触发断言失败,程序会中止执行并输出错误信息。

assert(pos >= 0 && pos <= sl->size):使用 assert 宏确保删除的位置 pos 合法。如果删除的位置不合法,assert 会触发断言失败,程序会中止执行并输出错误信息。

for (int i = pos; i < sl->size; i++):使用循环从指定位置开始,向后遍历到最后一个元素。

sl->a[i] = sl->a[i + 1]:将当前位置的数据向前移动一位,覆盖后一个位置的数据。

sl->size–:将顺序表的有效数据个数减一,表示删除了指定位置的数据。

查找

void SLSer(SL* sl,int pos)
{
	assert(sl);
	for (int i = sl->size; i >= 0; i--)
	{
		if (sl->a[i] == pos)
		{
			printf("%d\n", i);
		}
	}
}

assert(sl):使用 assert 宏确保传入的参数 sl 不为空。如果 sl 为空,assert
会触发断言失败,程序会中止执行并输出错误信息。

for (int i = sl->size; i >= 0; i–):使用循环从顺序表的最后一个元素开始,向前遍历到第一个元素。

if (sl->a[i] == pos):判断当前位置的数据是否等于指定数据。

printf(“%d\n”, i);:如果当前位置的数据等于指定数据,则输出该数据在顺序表中的位置。

代码合集

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int SLDataType;

typedef struct SeqList
{
	SLDataType* a;
	int size;		// 有效数据
	int capacity;	// 空间容量
}SL;

//初始化
void SLInit(SL* sl)
{
	assert(sl);
	sl->a = NULL;
	sl->size = 0;
	sl->capacity = 0;
}

//销毁
void SLDestroy(SL* sl)
{
	assert(sl);
	if (sl->a != NULL)
	{
		free(sl->a);
		sl->a = NULL;
		sl->size = 0;
		sl->capacity = 0;
	}
}

//打印
void SLPrint(SL* sl)
{
	assert(sl);
	for (int i = 0; i < sl->size; i++)
	{
		printf("%d ", sl->a[i]);
	}
	printf("\n");
}

//检查容量
void SLCheckCapacity(SL* sl)
{
	assert(sl);
	if (sl->size == sl->capacity)
	{
		int newCapacity = sl->capacity == 0 ? 4 : sl->capacity * 2;
		SLDataType* tmp = (SLDataType*)realloc(sl->a, sizeof(SLDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		sl->a = tmp;
		sl->capacity = newCapacity;
	}
}
//尾插
void SLPushBack(SL* sl,SLDataType k)
{
	assert(sl);
	SLCheckCapacity(sl);
	sl->a[sl->size ] = k;
	sl->size++;
}
//头插	
void SLPushFront(SL* sl, SLDataType k)
{
	assert(sl);
	SLCheckCapacity(sl);	
	for (int i = sl->size-1; i >= 0; i--)
	{
		sl->a[i + 1] = sl->a[i];
	}
	sl->a[0] = k;
	sl->size++;
}
//尾删
void SLPopBack(SL* sl)
{
	assert(sl);
	assert(sl->size > 0);
	sl->size--;
}
//头删
void SLPopFront(SL* sl)
{
	assert(sl);
	assert(sl->size > 0);
	for (int i = 0; i < sl->size; i++)
	{
		sl->a[i] = sl->a[i+1];
	}
	sl->size--;
}
//任意下标位置的插入
void SLInsertAnyWhere(SL* sl,int pos,SLDataType k)
{
	assert(sl);
	assert(pos >= 0 && pos <= sl->size);
	for (int i = sl->size - 1; i >= pos; i--)
	{
		sl->a[i + 1] = sl->a[i];
	}
	sl->a[pos] = k;
}
//任意下标位置的删除
void SLPopAnyWhere(SL* sl, int pos, SLDataType k)
{
	assert(sl);
	assert(pos >= 0 && pos <= sl->size);
	for (int i = pos; i < sl->size; i++)
	{
		sl->a[i] = sl->a[i + 1];
	}
	sl->size--;
}
//查找
void SLSer(SL* sl,int pos)
{
	assert(sl);
	for (int i = sl->size; i >= 0; i--)
	{
		if (sl->a[i] == pos)
		{
			printf("%d\n", i);
		}
	}
}

int main()
{
	SL sl;
	SLInit(&sl);
	//尾插
	printf("尾插测试\n");
	SLPushBack(&sl, 10);
	SLPushBack(&sl, 20);
	SLPushBack(&sl, 30);
	SLPushBack(&sl, 40);
	SLPushBack(&sl, 50);
	SLPrint(&sl);
	//头插
	printf("头插测试\n");
	SLPushFront(&sl, 9);
	SLPushFront(&sl, 8);
	SLPushFront(&sl, 7);
	SLPushFront(&sl, 6);
	SLPushFront(&sl, 5);
	SLPushFront(&sl, 4);
	SLPushFront(&sl, 3);
	SLPushFront(&sl, 2);
	SLPushFront(&sl, 1);
	SLPushFront(&sl, 1);
	SLPrint(&sl);
	//尾删
	printf("尾删一次测试\n");
	SLPopBack(&sl);
	SLPrint(&sl);
	printf("尾删两次测试\n");
	SLPopBack(&sl);
	SLPrint(&sl);
	//头删
	printf("头删测试\n");
	SLPopFront(&sl);
	SLPrint(&sl);
	//任意下标位置插入
	printf("下标为1的位置插入测试\n");
	SLInsertAnyWhere(&sl, 1,2);
	SLPrint(&sl);
	//任意下标位置的删除
	printf("下标为1的位置删除测试\n");
	SLPopAnyWhere(&sl, 1, 2);
	SLPrint(&sl);
	//查找测试
	printf("查找数值为5的下标位置\n");
	SLPrint(&sl);
	SLSer(&sl, 5);
	return 0;
}

运行结果
在这里插入图片描述

4. 顺序表的优缺点分析

- 优点:顺序表的性能优势

  1. 访问速度快:由于顺序表使用连续的内存空间存储元素,并且可以通过下标直接访问元素,因此访问速度非常快。无需遍历或搜索就能够定位和访问指定位置的元素。

  2. 节省存储空间:相比于链表等非顺序存储结构,顺序表使用的是连续的内存空间,不需要额外的指针来连接元素,因此节省了存储空间。

  3. 支持随机访问:顺序表可以根据下标进行随机访问,即可以根据位置直接访问元素。这对于需要频繁按照位置访问元素的场景非常高效。

- 缺点:顺序表的局限性和可能的改进空间

  1. 插入和删除操作耗时:当需要在顺序表中插入或删除元素时,可能需要移动其他元素,导致操作耗时。插入和删除操作的时间复杂度为 O(n),其中 n 是元素的数量。

  2. 固定大小:顺序表的大小是固定的,一旦分配了固定大小的内存空间,就无法动态调整大小。如果元素的数量超过了预分配的空间大小,就需要重新分配更大的空间,进行数据的搬迁,这可能会导致额外的开销。

  3. 不适合频繁插入和删除操作:由于插入和删除操作需要移动其他元素,因此在需要频繁执行插入和删除操作的场景下,顺序表的性能可能较差。

为了克服顺序表的一些缺点,可以考虑以下改进空间:

  1. 引入动态扩容机制:当顺序表的元素数量超过预分配的空间大小时,可以动态扩容,重新分配更大的空间,并将原有的元素复制到新的空间中,以适应更多的元素。

  2. 使用链表实现:对于频繁插入和删除操作较多的场景,可以考虑使用链表实现,链表的插入和删除操作时间复杂度为 O(1)。

5. 顺序表的应用场景

- 实际应用:顺序表在实际项目中的应用案例

  1. 数组:数组是一种顺序表的特殊形式,可以用于存储和处理一组具有相同数据类型的元素。在各种编程语言中,数组作为基本的数据结构被广泛应用,用于解决各种问题。

  2. 数据库索引:数据库中的索引通常使用顺序表来实现。通过将索引字段的值按照一定的顺序存储在顺序表中,可以加快数据库的检索速度,提高查询效率。

  3. 缓存管理:在计算机系统中,顺序表常被用作缓存的数据结构。通过将最常访问的数据存储在顺序表中,可以提高数据的访问速度,减少对底层存储的访问次数。

  4. 排序算法:许多排序算法,如冒泡排序、插入排序等,都可以通过顺序表来实现。顺序表提供了随机访问的能力,使得排序算法的实现更加简单和高效。

- 对比分析:与其他数据结构在实际应用中的对比情况

  1. 访问速度:与链表等非顺序存储结构相比,顺序表的访问速度更快。由于顺序表使用连续的内存空间存储元素,并且可以通过下标直接访问元素,因此在需要频繁访问元素的场景下,顺序表的性能更好。

  2. 存储空间:相比于链表等需要额外指针来连接元素的数据结构,顺序表的存储空间更加紧凑。顺序表不需要额外的指针来连接元素,节省了存储空间。

  3. 插入和删除操作:顺序表的插入和删除操作可能比链表等数据结构耗时,特别是需要移动其他元素的情况下。但对于随机访问和按位置访问较多的场景,顺序表仍然是一种合适的选择。

  4. 动态调整大小:相比于顺序表,链表等数据结构具有动态调整大小的能力。链表可以根据实际需要动态分配和释放内存空间,而顺序表的大小是固定的。

6. 顺序表的扩展与优化

- 动态扩容:如何处理顺序表空间不足的情况?

当顺序表空间不足时,需要进行动态扩容操作,以便能够继续存储更多的元素。一般来说,可以采取以下步骤来处理顺序表空间不足的情况:

  1. 重新分配内存空间:首先,需要重新分配一个更大的内存空间来存储元素。可以通过动态内存分配的方式(如realloc函数)来实现这一步骤。

  2. 数据迁移:接着,需要将当前顺序表中的元素迁移到新的内存空间中。这包括将原有的元素复制到新的内存空间,并释放原有的内存空间。

  3. 更新指针或引用:最后,需要更新指向顺序表的指针或引用,使其指向新的内存空间。

动态扩容的关键在于及时检测顺序表空间不足的情况,并采取合适的措施来扩充内存空间。一种常见的做法是在插入元素时检查当前空间是否足够,若不足则按照一定的策略进行扩容操作,通常是以一定的比例增加现有空间大小,以减少频繁扩容的开销。

- 性能优化:针对顺序表常见操作的性能优化方法

针对顺序表的常见操作,可以采取一些性能优化方法来提高其操作效率:

  1. 随机访问优化:顺序表的优势之一是能够进行快速的随机访问,但为了进一步提高访问速度,可以考虑使用局部性原理来优化。例如,可以利用CPU缓存的特性,将经常访问的数据预先加载到缓存中,以减少内存访问的时间。

  2. 插入和删除优化:虽然顺序表的插入和删除操作可能较慢,但可以通过一些技巧来进行优化。例如,针对频繁的插入和删除操作,可以考虑使用“延迟复制”或“分块复制”等技术,减少数据移动的次数。

  3. 内存预分配:为了减少动态扩容操作的频率,可以在创建顺序表时预先分配一定大小的内存空间,以减少频繁的内存重新分配和数据迁移操作。

  4. 数据压缩:针对稀疏性较大的顺序表,可以考虑使用数据压缩技术,将稀疏区域的内存空间释放出来,以节省存储空间。

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

阿原学编程

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

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

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

打赏作者

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

抵扣说明:

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

余额充值