数据结构:顺序表讲解(总)

前言

本篇文章将讲解顺序表介绍,顺序表分类,顺序表动态顺序表的实现,顺序表算法题,顺序表问题与思考知识的相关内容,本文为数据结构:顺序表讲解(1)与数据结构:顺序表讲解(2)文章知识的整合。

一、顺序表介绍

介绍:

在说明顺序表之前,我们要先了解下线性表的概念:

1.线性表

线性表:逻辑结构的统称

线性表是n个具有相同特性的数据元素的有限序列,是一种抽象的逻辑结构,仅描述数据元素之间的"线性"关系(即除首尾元素外,每个元素有唯一前驱和后继)。其核心特征包括:

  • 逻辑连续性:元素按顺序排列,形成一条直线。
  • 有限性:元素个数确定,非无限集合。
  • 通用性:可表示多种实际问题,如学生名单、购物清单、队列等。

常见类型

  • 顺序表:用连续内存存储(如数组)。
  • 链表:用非连续节点存储(通过指针/引用链接)。
  • 栈/队列:特殊线性表(操作受限)。
  • 字符串:元素为字符的线性表。

线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

  1. 逻辑结构:线性表的本质定义

    • 线性表的逻辑结构必须是线性的(元素间"一对一"相邻关系),这是其核心特征。
  2. 物理结构:逻辑结构的存储实现

    • 物理结构(存储方式)可以是线性的,也可以是非线性的 ,但需保证能体现逻辑上的线性关系:
      • 线性物理结构:如数组(顺序存储),元素在内存中连续存放,物理位置直接反映逻辑顺序。
      • 非线性物理结构:如链表(链式存储),元素在内存中分散存放,通过指针/引用关联,逻辑顺序通过指针体现(物理上不连续,但逻辑上线性)。

我们可以理解为: 线性表 = 逻辑结构(线性)+ 物理结构(可选线性/非线性存储)

2.顺序表概念与结构

顺序表是⽤⼀段物理地址连续的存储单元依次存储数据元素的线性结构,⼀般情况下采⽤数组存储。

根据(1)的概念,我们可以得知:顺序表的逻辑结构和物理结构均为线性的。

顺序表的底层结构是数组,对数组的封装,实现了常⽤的增删改查等接⼝。

根据此定义,我们可以描绘下顺序表:

如图:

既然上文说过:顺序表的底层结构是数组,对数组的封装。

那么二者有何区别:

  1. 首先,数组是一种物理存储结构 ,指用一段连续的内存空间存储相同数据类型 元素的集合。其核心作用是存储数据,不自带数据管理功能。
  2. 顺序表是一种逻辑结构(线性表)的实现方式 ,底层基于数组,但通过结构体封装 ,提供了对数据的管理功能(如动态扩容、增删查改接口)。

简单来说:我们可以认为:数组是基础,顺序表是"升级版"。更加方便且好用。

  • 数组 ≈ "空饭盒":仅提供存放食物的空间,需手动装取。
  • 顺序表 ≈ "智能饭盒":自带分区、加热、容量提示功能,方便管理食物。

二、顺序表分类

介绍:

顺序表根据底层数组的特性分为两类,均需通过结构体封装:

分为静态顺序表与动态顺序表。

1.静态顺序表

所谓静态顺序表,实质为使用定长数组来存储元素,他的空间在一开始时就是确定好了的,空间给少了不够⽤,给多了造成空间浪费, 上面提到过顺序表通过结构体封装实现,静态顺序表为:(例)

struct A

{

int a[100]; //定长数组

int size; //有效的数据个数

}; (数组类型可不止int的,也可以为其他类型)

结合代码内容,我们可以近似得出图示:

2.动态顺序表

所谓动态顺序表,是一种基于数组实现的线性数据结构,其大小可根据元素数量动态调整(扩容/缩容),克服了静态顺序表容量固定的缺点。

核心特点
  • 底层结构 :动态分配的数组(通过malloc/realloc管理内存)。
  • 动态性:容量不足时自动扩容(通常按原容量的2倍或1.5倍增长)。
  • 随机访问:支持O(1)时间复杂度的元素访问(通过下标)。
  • 顺序存储:元素在内存中连续存放。

动态顺序表为:

struct A

{

int* a; // 动态数组

int size; //有效数据个数

int capacity; //空间容量大小

}; (数组类型可不止int的,也可以为其他类型)

结合代码内容,我们可以近似得出图示:

由于静态顺序表使用起来不太方便,空间给少了不够⽤,给多了造成空间浪费,而动态顺序表,是一种基于数组实现的线性数据结构,其大小可根据元素数量动态调整(扩容/缩容),克服了静态顺序表容量固定的缺点,所以我们顺序表方面知识重点讲解动态顺序表部分。

三、动态顺序表的实现

讲解

接下来,我会讲解动态顺序表中会涉及的函数以及他们的代码实现:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
//定义顺序表的结构
typedef int type;
typedef struct Seqlist
{
	type* arr;//存储数据
	int size;//有效数据个数
	int capacity;//空间容量
}SL;
  • 核心成员解析
    • type* arr:动态数组的指针。顺序表的本质是用数组存储数据,通过指针管理内存空间。
    • size :记录当前已使用的元素个数(从0开始计数,size-1是最后一个元素的下标)。
    • capacity :记录数组的总容量。当size == capacity时,需扩容(通过realloc重新分配更大的内存)。

1.初始化: SLinit

void SLinit(SL* h)

cpp 复制代码
void SLinit(SL* h)
{
	h->arr = NULL;
	h->capacity = h->size = 0;
}
  • h->arr = NULL; // 初始时不分配内存,指针置空
  • h->capacity = 0; // 初始容量为0(无可用空间)
  • h->size = 0; // 初始有效数据个数为0(无元素)

初始化部分简单来说就是像数组刚刚创建,没有存值的情况,内部没有内容,数组指针则置为NULL,两位整数,给值0;

在使用时:这样调用函数即可:

一定要采用传址调用的形式,否则会出现一些错误,因为传值调用修改形参并不影响实参,它们的地址终究还是不同的,而传址调用,形参的修改会影响实参,而我们的目的,是通过传址调用,形参的修改影响实参。

2.顺序表的尾插

前文曾说过,顺序表的底层为数组,所以我们可以先想想数组的尾插入是什么样的:

尾差为直接在数组的结尾处插入数据,顺序表也同理:

但首先,我们要考虑一下问题:

  1. 初始化中,明确的将值放置为NULL ,0值,插入操作该先怎么办。
  2. 作为动态顺序表,如果当前容量满了,我们该怎么办。

解决办法: 调用扩容函数

void kuo(SL* h);

不太会起函数名哈;

cpp 复制代码
void kuo(SL* h)
{
	if (h->capacity == h->size)
	{
		int newcapacity = h->capacity == 0 ? 4 : 2 * h->capacity;
		type* tmp = (type*)realloc(h->arr, sizeof(type) * newcapacity);
		if (tmp)
		{
			h->arr = tmp;
			h->capacity = newcapacity;
		}
		else
		{
			exit(1);
		}
	}
}

解释:

核心目标 :当顺序表已满(size == capacity)时,扩大内存空间。

1. 扩容触发条件

cpp 复制代码
if (h->capacity == h->size) // 容量已满,需要扩容
  • h->size:当前有效元素个数(已使用空间)。
  • h->capacity:当前总容量(已分配空间)。
  • 当两者相等时,说明数组已满,插入新元素会导致越界,必须先扩容。

2. 计算新容量 newcapacity

cpp 复制代码
int newcapacity = h->capacity == 0 ? 4 : 2 * h->capacity;

增容:我们一般成倍数增加,最好是两倍。这样可以有效降低增容的次数,就算可能会存在空间的浪费,但是也不会有太多浪费。

  • 三目运算符逻辑
    • 若初始容量为0(h->capacity == 0,即首次扩容),则新容量设为 4(默认初始值,可自定义,如8)。
    • 若已有容量(如4),则新容量设为 原容量的2倍2 * h->capacity,例如4→8,8→16)。
  • 为什么2倍扩容?
    • 时间效率:分摊扩容成本。若每次只扩1个空间,频繁插入会导致多次扩容(每次扩容时间复杂度O(n)),总时间复杂度退化到O(n²);
    • 2倍扩容:每次扩容后可支持多次插入,分摊后单次插入的平均时间复杂度为O(1)(均摊分析)。

3. 动态内存分配(realloc

cpp 复制代码
type* tmp = (type*)realloc(h->arr, sizeof(type) * newcapacity);
  • realloc 功能
    • 重新分配 h->arr 指向的内存空间,新大小为 sizeof(type) * newcapacity(总字节数 = 单个元素大小 × 新容量)。
    • h->arrNULL(首次扩容),realloc 等价于 malloc(直接分配新空间)。
  • 临时指针 tmp
    • 避免直接修改 h->arr。若 realloc 失败(内存不足),会返回 NULL,此时若直接赋值给 h->arr,会导致原内存地址丢失(内存泄漏)。

4. 扩容结果处理

cpp 复制代码
if (tmp) {                  // 扩容成功
    h->arr = tmp;           // 更新数组指针到新内存
    h->capacity = newcapacity;  // 更新容量为新容量
} else {                    // 扩容失败(内存不足)
    exit(1);                // 终止程序(实际开发中可返回错误码)
}
  • 成功时 :更新顺序表的 arr(指向新内存)和 capacity(记录新容量)。
  • 失败时realloc 返回 NULL,此时程序无法继续运行(无空间存储新元素),通过 exit(1) 退出(1 表示异常退出)。

本函数很有用,凡是插入值得函数,都要先调用一下函数的,作为检测。

此时尾插入函数就好写了:

void SLpushback(SL* h, type x)

cpp 复制代码
void SLpushback(SL* h, type x)
{
	kuo(&h);
	h->arr[h->size++] = x;
}

函数调用:

3.顺序表的头插

前文曾说过,顺序表的底层为数组,所以我们可以先想想数组的头插入是什么样的:

数组想头部插入一个值,做法是进行数据的移动,将数据整体的向后移动一位,顺序表也是如此,但头插入操作时,也需要考虑下扩容的。

void kuo(SL* h);

cpp 复制代码
void kuo(SL* h)
{
	if (h->capacity == h->size)
	{
		int newcapacity = h->capacity == 0 ? 4 : 2 * h->capacity;
		type* tmp = (type*)realloc(h->arr, sizeof(type) * newcapacity);
		if (tmp)
		{
			h->arr = tmp;
			h->capacity = newcapacity;
		}
		else
		{
			exit(1);
		}
	}
}

接下来,写一下头插入函数:

void SLpushfront(SL* h, type x)

cpp 复制代码
void SLpushfront(SL* h, type x)
{
	kuo(h);
	int i;
	for (i = h->size; i > 0; i--)
	{
		h->arr[i] = h->arr[i - 1];
	}
	h->arr[i] = x;
	h->size++;
}

注意:头插,顺序表中的其它元素一定是从后往前的顺序向后移动一位。

4.顺序表的尾删

尾部删除一个值,其实并不难,不需要使用free释放掉或是令h->arr[h->size-1]=0,然后h->size--,我们只要将当前记录节点个数减一即可,这样访问时便访问不到该值,在之后如果插入值,会覆盖掉我们认为删去的值的。

void popback(SL* h);

cpp 复制代码
void popback(SL* h)
{
	h->size--;
}

我们也可以更谨慎些:

cpp 复制代码
void popback(SL* h)
{
	assert(h && h->size);
	h->size--;
}
  • 使用 assert(h && h->size) 确保:
    • h 不为空指针(避免对空指针解引用);
    • 顺序表当前 size > 0(避免删除空表,防止 size 那里出现问题)。

记住要有#include<assert.h> 头文件。

5.顺序表的头删

头删与尾删和头插入、尾部插入的操作相反,既然头插入是整体的像后方移动一位,那我们可以通过(除了头节点)整体的向前方移动一位,来实现头部删除。

void popfront(SL* h);

cpp 复制代码
void popfront(SL* h)
{
	assert(h && h->size);
	int i;
	for (i = 0; i < h->size - 1; i++)
	{
		h->arr[i] = h->arr[i + 1];
	}
	h->size--;
}
  1. 通过 for 循环(i 从 0 到 size-2)将数组中 从第2个元素开始的所有元素依次向前移动一位,覆盖头部元素(arr[0])。
  2. 移动完成后,h->size-- 减少顺序表长度,原头部元素被删除。

四、尾插,头插,尾删,头删时间复杂度对比:

1.尾插入:

该插入为直接找到位置,直接在位置处插入值,复杂度O(1)。

2.头插入:

该插入为将该位置起的节点整体后移一位,之后在首位置处插入值,经历了一次for循环,复杂度O(n)。

3.尾删:

该删除为直接使节点数减一,复杂度O(1)。

4.头删:

该插入为将该首位置后的节点整体前移一位,之后首位置处值被覆盖,经历了一次for循环,复杂度O(n)。

五、动态顺序表的实现(继续)

在上篇博客中我们已经完成了顺序表的头插,尾插,头删,尾删等操作( 数据结构:顺序表讲解(1)-CSDN博客),本篇博客将讲解剩余的操作:

1.顺序表的指定位置查找

int Find(SL* h, type x);

cpp 复制代码
int Find(SL* h, type x)
{
	assert(h);
	int i;
	for (i = 0; i < h->size; i++)
	{
		if (h->arr[i] == x)
		{
			return i;
	    }
	}
	return -1;
}

解释:

  • 该函数用于在顺序表(SL)中查找指定元素 x,返回其下标(找到时)或 -1(未找到时),核心逻辑为遍历数组比较元素。

根据上篇文章:我们已经了解了尾插,头插,尾删,头删的内容,我们可以先实现一下代码操作:

但是我们在尾插,头插等操作时使用了malloc函数动态开辟内存,在结束程序时需要释放下开辟的空间,所以需要有销毁函数。

2.顺序表的销毁

void SLDestory(SL* h)

cpp 复制代码
void SLDestory(SL* h)
{
	if (h->arr)
		free(h->arr);
	h->arr = NULL;
	h->size = 0;
	h->capacity = 0;
}

解释:

  • SLDestory 是顺序表的销毁函数,用于释放动态分配的内存资源并重置结构状态,核心目标是避免内存泄漏
  • 作用h->arr 是顺序表中存储元素的动态数组(通过 malloc/calloc 分配),free(h->arr) 会将其占用的内存归还给系统。
  • 为什么加 if (h->arr)
    • h->arrNULL(空表或已释放),free(NULL) 在 C 语言中是安全的(无操作),因此此判断非必需 ,可简化为直接 free(h->arr)

结合之前的代码举例:(我写了3个文件)

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义顺序表的结构
typedef int type;
typedef struct Seqlist
{
	type* arr;//存储数据
	int size;//有效数据个数
	int capacity;//空间容量
}SL;
void SLinit(SL* h);
void kuo(SL* h);
void SLpushback(SL* h,type x);
void SLpushfront(SL* h, type x);
void popback(SL* h); 
void popfront(SL* h);
int Find(SL* h, type x);
void SLDestory(SL* h);
void print(SL* h);
cpp 复制代码
//定义顺序表的结构
#include"1.h"
void SLinit(SL* h)
{
	h->arr = NULL;
	h->capacity = h->size = 0;
}
void kuo(SL* h)
{
	if (h->capacity == h->size)
	{
		int newcapacity = h->capacity == 0 ? 4 : 2 * h->capacity;
		type* tmp = (type*)realloc(h->arr, sizeof(type) * newcapacity);
		if (tmp)
		{
			h->arr = tmp;
			h->capacity = newcapacity;
		}
		else
		{
			exit(1);
		}
	}
}

void SLpushback(SL* h, type x)
{
	kuo(h);
	h->arr[h->size++] = x;
}
void SLpushfront(SL* h, type x)
{
	kuo(h);
	int i;
	for (i = h->size; i > 0; i--)
	{
		h->arr[i] = h->arr[i - 1];
	}
	h->arr[i] = x;
	h->size++;
}
void popback(SL* h)
{
	assert(h && h->size);
	h->size--;
}
void popfront(SL* h)
{
	assert(h && h->size);
	int i;
	for (i = 0; i < h->size - 1; i++)
	{
		h->arr[i] = h->arr[i + 1];
	}
	h->size--;
}
int Find(SL* h, type x)
{
	assert(h);
	int i;
	for (i = 0; i < h->size; i++)
	{
		if (h->arr[i] == x)
		{
			return i;
	    }
	}
	return -1;
}
void SLDestory(SL* h)
{
	if (h->arr)
		free(h->arr);
	h->arr = NULL;
	h->size = 0;
	h->capacity = 0;
}
void print(SL* h)
{
	int i;
	for (i = 0; i < h->size; i++)
	{
		printf("%d  ", h->arr[i]);
	}
	printf("\n");
}
cpp 复制代码
#include"1.h"
void test1()
{
	SL h;
	SLinit(&h);
	SLpushback(&h, 1);  //1
	SLpushback(&h, 2);  //12
	SLpushback(&h, 3);  //123
	SLpushback(&h, 4);  //1234
	SLpushfront(&h, 1);  //11234
	SLpushfront(&h, 2);  //211234
	SLpushfront(&h, 3);  //3211234
	SLpushfront(&h, 4);  //43211234
	print(&h);
	popback(&h);         //4321123
	print(&h);
	popback(&h);         //432112
	print(&h);
	popfront(&h);         //32112
	print(&h);
	popfront(&h);         //2112
	print(&h);
	SLDestory(&h);
}
int main()
{
	test1();
}

结果为:

3.顺序表的指定位置插入

实现指定位置插入,顺序表的指定位置插入是指在数组的第 pos 个下标处插入新元素,核心操作是移动元素 + 赋值,需处理扩容、边界检查等关键步骤,接下来,我将展示代码:

void SLinsert(SL* h, int pos, type x);

cpp 复制代码
//在pos前插入
void SLinsert(SL* h, int pos, type x)
{
	assert(h);
	assert(pos >= 0 && pos <= h->size);
    kuo(h);
	for (int i = h->size; i > pos; i--)
	{
		h->arr[i] = h->arr[i - 1];
	}
	h->arr[pos] = x;
	h->size++;
}

讲解:

  • 插入位置 pos 的合法范围应为 [0, size](含 size),即允许在表尾插入。因此断言应为:

    cpp 复制代码
    assert(pos >= 0 && pos <= h->size);  // 允许在表尾(pos=size)插入

1. 元素移动逻辑

cpp 复制代码
for (int i = h->size; i > pos; i--)  // 从后往前移动元素
{
    h->arr[i] = h->arr[i - 1];  // 将arr[i-1]的值赋给arr[i]
}
  • 移动方向 :从 size 开始到 pos+1,正确(避免数据覆盖)。
  • 示例 :若原表为 [1,2,3]size=3),pos=1 时:
    • i=3arr[3] = arr[2](3 → 4号位置)
    • i=2arr[2] = arr[1](2 → 3号位置)
    • 结果:[1, _, 2, 3]pos=1 处空出,可插入新元素。

2. 插入元素与更新 size

cpp 复制代码
h->arr[pos] = x;  // 在pos位置插入新元素(正确)
h->size++;        // 有效元素个数+1(正确)

代码举例:

cpp 复制代码
#include"1.h"
void test1()
{
	SL h;
	SLinit(&h);
	SLpushback(&h, 1);  //1
	SLpushback(&h, 2);  //12
	SLpushback(&h, 3);  //123
	SLpushback(&h, 4);  //1234
	SLpushfront(&h, 1);  //11234
	SLpushfront(&h, 2);  //211234
	SLpushfront(&h, 3);  //3211234
	SLpushfront(&h, 4);  //43211234
	print(&h);
	popback(&h);         //4321123
	print(&h);
	popback(&h);         //432112
	print(&h);
	popfront(&h);         //32112
	print(&h);
	popfront(&h);         //2112
	print(&h);
	SLinsert(&h, 2, 55);  //2  1  55  1  2
	print(&h); 
	SLinsert(&h, 3, 125);  //2  1  55  125  1  2
	print(&h);
	SLDestory(&h);
}
int main()
{
	test1();
}

4.顺序表的指定位置删除

顺序表的指定位置删除是指移除数组中第 pos 个下标处的元素,核心操作是移动元素覆盖待删除值。接下来,我将展示该部分的代码以及实现:

void SLerase(SL* h, int pos)

cpp 复制代码
void SLerase(SL* h, int pos)
{
	assert(h);
	assert(pos >= 0 && pos < h->size);
	int i;
	for (i = pos;i<h->size-1;i++)
	{
		h->arr[i] = h->arr[i+1];
	}
	h->size--;
}

解析:

代码逐行讲解

1. 参数与断言检查
cpp 复制代码
assert(h);  // 确保顺序表指针h非空(必须,避免空指针访问)
assert(pos >= 0 && pos < h->size);  // 检查删除位置pos的合法性
  • pos 合法性pos 必须在 [0, size-1] 范围内(例如 size=3 时,可删除 0、1、2 位置,不能删除 pos=3)。
  • 隐含检查pos < h->size 已隐含 size > 0(若 size=0pos < 0 会触发断言失败),无需额外检查 h->size > 0
2. 元素前移覆盖(核心逻辑)
cpp 复制代码
for (i = pos; i < h->size - 1; i++)  // 循环条件:i从pos到size-2
{
    h->arr[i] = h->arr[i + 1];  // 将后一个元素(i+1)覆盖当前元素(i)
}
  • 循环范围i < h->size - 1 等价于 i <= h->size - 2,即从 pos 移动到倒数第二个元素size-2)。

    • 示例 :若原表为 [1,2,3,4]size=4),删除 pos=1(值为2):
      • i=1arr[1] = arr[2](3 → 2号位置,表变为 [1,3,3,4]
      • i=2arr[2] = arr[3](4 → 3号位置,表变为 [1,3,4,4]
      • 循环结束(i=33 < 4-1 不成立)。
  • 为什么是 size-1

    • i < h->size(错误条件),当 i = size-1 时,i+1 = size 会访问越界内存 (数组最大下标为 size-1)。
    • 原代码 size-1 确保 i+1 最大为 size-1(合法),避免越界。
3. 更新有效元素个数
cpp 复制代码
h->size--;  // 有效元素个数-1(逻辑删除,无需释放内存)
  • 效果 :删除后顺序表实际存储的有效元素为前 size-1 个(覆盖后最后一个元素 arr[size-1] 变为无效数据,但不影响访问)。

例:

cpp 复制代码
SLinsert(&h, 3, 125);  //2  1  55  125  1  2
print(&h);
SLerase(&h, 3);
print(&h);             //2  1  55  1  2 将下标为3的删去

5.实现的总代码

本代码,我通过3个文件来实现的,分别为头文件(1.h)、实现文件(1.c)、测试文件(maic.c)

代码如下:

1.h

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义顺序表的结构
typedef int type;
typedef struct Seqlist
{
	type* arr;//存储数据
	int size;//有效数据个数
	int capacity;//空间容量
}SL;
void SLinit(SL* h);
void kuo(SL* h);
void SLpushback(SL* h,type x);
void SLpushfront(SL* h, type x);
void popback(SL* h); 
void popfront(SL* h);
int Find(SL* h, type x);
void SLDestory(SL* h);
void print(SL* h);
void SLinsert(SL* h, int pos, type x);
void SLerase(SL* h, int pos);

1.c

cpp 复制代码
//定义顺序表的结构
#include"1.h"
void SLinit(SL* h)
{
	h->arr = NULL;
	h->capacity = h->size = 0;
}
void kuo(SL* h)
{
	if (h->capacity == h->size)
	{
		int newcapacity = h->capacity == 0 ? 4 : 2 * h->capacity;
		type* tmp = (type*)realloc(h->arr, sizeof(type) * newcapacity);
		if (tmp)
		{
			h->arr = tmp;
			h->capacity = newcapacity;
		}
		else
		{
			exit(1);
		}
	}
}

void SLpushback(SL* h, type x)
{
	kuo(h);
	h->arr[h->size++] = x;
}
void SLpushfront(SL* h, type x)
{
	kuo(h);
	int i;
	for (i = h->size; i > 0; i--)
	{
		h->arr[i] = h->arr[i - 1];
	}
	h->arr[i] = x;
	h->size++;
}
void popback(SL* h)
{
	assert(h && h->size);
	h->size--;
}
void popfront(SL* h)
{
	assert(h && h->size);
	int i;
	for (i = 0; i < h->size - 1; i++)
	{
		h->arr[i] = h->arr[i + 1];
	}
	h->size--;
}
int Find(SL* h, type x)
{
	assert(h);
	int i;
	for (i = 0; i < h->size; i++)
	{
		if (h->arr[i] == x)
		{
			return i;
	    }
	}
	return -1;
}
void SLDestory(SL* h)
{
	if (h->arr)
		free(h->arr);
	h->arr = NULL;
	h->size = 0;
	h->capacity = 0;
}
void print(SL* h)
{
	int i;
	for (i = 0; i < h->size; i++)
	{
		printf("%d  ", h->arr[i]);
	}
	printf("\n");
}
//在pos前插入
void SLinsert(SL* h, int pos, type x)
{
	assert(h);
	assert(pos >= 0 && pos <= h->size);
	kuo(h);
	for (int i = h->size; i > pos; i--)
	{
		h->arr[i] = h->arr[i - 1];
	}
	h->arr[pos] = x;
	h->size++;
}
void SLerase(SL* h, int pos)
{
	assert(h);
	assert(pos >= 0 && pos < h->size);
	int i;
	for (i = pos;i<h->size-1;i++)
	{
		h->arr[i] = h->arr[i+1];
	}
	h->size--;
}

main.c

cpp 复制代码
#include"1.h"
void test1()
{
	SL h;
	SLinit(&h);
	SLpushback(&h, 1);  //1
	SLpushback(&h, 2);  //12
	SLpushback(&h, 3);  //123
	SLpushback(&h, 4);  //1234
	SLpushfront(&h, 1);  //11234
	SLpushfront(&h, 2);  //211234
	SLpushfront(&h, 3);  //3211234
	SLpushfront(&h, 4);  //43211234
	print(&h);
	popback(&h);         //4321123
	print(&h);
	popback(&h);         //432112
	print(&h);
	popfront(&h);         //32112
	print(&h);
	popfront(&h);         //2112
	print(&h);
	SLinsert(&h, 2, 55);  //2  1  55  1  2
	print(&h); 
	SLinsert(&h, 3, 125);  //2  1  55  125  1  2
	print(&h);
	SLerase(&h, 3);
	print(&h);             //2  1  55  1  2 将下标为3的删去
	SLDestory(&h); 
}
int main()
{
	test1();
}

六、顺序表算法题

根据我们讲解的知识点,我们来看几道关于该知识的算法题:

1.移除元素

27. 移除元素 - 力扣(LeetCode)

本题思路:我们可以选用双指针法来实现该题,具体操作为:我们在开始时创建两个int变量,src,dst,通过src(数组下标)遍历整个数组,如果发现数组值不为val,我们则将值传入到以dst为下标的数组中,如此情况: 这是简单的讲法,接下来,我将通过代码来讲解:

代码:

cpp 复制代码
int removeElement(int* nums, int numsSize, int val) {
    int src=0,dst=0;
    while(src<numsSize)
    {
        if(nums[src]!=val)
        {
            nums[dst]=nums[src];
            dst++;
        }
       src++;
    }
    return dst;
}

详细讲解:

该算法空间复杂度为 O(1),时间复杂度为O(n)。

核心逻辑:双指针法

通过两个指针 src(源指针)和 dst(目标指针)遍历数组,将不等于 val 的元素"搬运"到数组前端,覆盖需要移除的元素。

  • src :遍历整个数组,寻找不等于 val 的元素。
  • dst :指向当前需要填充的位置,仅在 src 找到有效元素时移动。
步骤分解
  1. 初始化指针src = 0dst = 0(均从数组起始位置开始)。
  2. 遍历数组src 从 0 到 numsSize-1 循环:
    • nums[src] != val:将 nums[src] 赋值给 nums[dst]dst 后移一位(指向下一个待填充位置)。
    • nums[src] == valdst 不动(跳过该元素,后续被覆盖)。
    • 无论是否匹配,src 始终后移一位。
  3. 返回新长度dst 的值即为移除后有效元素的个数(因为 dst 每填充一个有效元素就 +1)。

2.删除有序数组中的重复项

26. 删除有序数组中的重复项 - 力扣(LeetCode)

本题思路:我们可以选用双指针法来实现该题,具体操作为:我们在开始时创建两个int变量,src=0,dst=0,通过src(数组下标)遍历整个数组(这一部分与上一题基本相同,但后来操作上有较大不同),如果发现数组值不重复,我们则将值传入到以dst为下标的数组中,忽视掉重复的值,如此情况: 这是简单的讲法,接下来,我将通过代码来讲解:

cpp 复制代码
int removeDuplicates(int* nums, int numsSize) {
    int src=0,dst=0;
    while(src<numsSize)
    {
        if(nums[dst]!=nums[src])
        {
            nums[++dst]=nums[src];
        }
        src++;
    }
    return dst+1;
}

详细讲解:

该函数用于移除非严格递增排序数组中的重复元素(只保留一个),返回移除后数组的新长度,空间复杂度为 O(1)(原数组上修改)。

核心逻辑:双指针法(快慢指针)

利用数组已排序 的特性,通过两个指针 src(快指针)和 dst(慢指针)遍历数组,仅保留不重复的元素。

  • src :遍历整个数组,寻找与 dst 指向元素不同的新元素。
  • dst :指向当前已确认的不重复元素的最后一个位置,仅在 src 找到新元素时移动。

步骤分解

  1. 初始化指针src = 0dst = 0(均从数组起始位置开始)。
  2. 遍历数组src 从 0 到 numsSize-1 循环:
    • nums[dst] != nums[src]:说明 src 找到新的不重复元素,将 dst 先 +1(移动到下一个待填充位置),再将 nums[src] 赋值给 nums[dst]
    • nums[dst] == nums[src]dst 不动(跳过重复元素)。
    • 无论是否重复,src 始终后移一位。
  3. 返回新长度dst + 1(因为 dst 指向最后一个有效元素的索引,长度需 +1)。

3.合并两个有序数组

88. 合并两个有序数组 - 力扣(LeetCode)

本题思路:题目的要求挺多,注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。 此处提示我们要利用好原数组的空间,所以,我们本题的解法围绕着**nums1**

后解法:三指针法: 创建三个整形指针,例:s1,s2,s3 分别指向:1数组的头,尾部,2数组的尾部,之后再进行操作:

  • 即比较两数组下标s1,s3值对应的大小,nums1[s1] < nums2[s3]:将 nums2[s3] 放入 nums1[s2],s3 和 s2 均左移一位(s3--,s2--)。
  • 否则:将 nums1[s1] 放入 nums1[s2],s1 和 s2 均左移一位(s1--,s2--)。
  • 处理剩余元素:若 nums2 仍有未合并的元素(s3 >= 0),直接将剩余元素依次放入 nums1 的头部(此时 nums1 的有效元素已全部合并,s1 已遍历完,s2 指向剩余空位的起始位置)。

具体操作通过代码讲解:

特殊例:

cpp 复制代码
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) 
{  int s1=m-1;
int s2=n+m-1;
int s3=n-1;
while(s1>=0&&s3>=0)
{
     if(nums1[s1]<nums2[s3])
 nums1[s2--]=nums2[s3--];
else
 nums1[s2--]=nums1[s1--];
}
 if(s3>=0)
 { while(s3>=0)
 {
     nums1[s2--]=nums2[s3--];
 }
 }  
}

总结

以上就是今天要讲的内容,本篇文章涉及的知识点为:讲解顺序表介绍,顺序表分类,顺序表动态顺序表的实现,顺序表算法题,顺序表问题与思考知识的相关内容,为本章节知识的内容,希望大家能喜欢我的文章,谢谢各位,接下来的内容我会很快更新有关链表的知识。

相关推荐
Miraitowa_cheems8 小时前
LeetCode算法日记 - Day 82: 环形子数组的最大和
java·数据结构·算法·leetcode·决策树·线性回归·深度优先
Code_Shark9 小时前
AtCoder Beginner Contest 426 题解
数据结构·c++·算法·数学建模·青少年编程
仰泳的熊猫9 小时前
LeetCode:698. 划分为k个相等的子集
数据结构·c++·算法·leetcode
豐儀麟阁贵9 小时前
4.5数组排序算法
java·开发语言·数据结构·算法·排序算法
Jane-6667779 小时前
C语言——栈与队列
c语言·开发语言
richxu2025100110 小时前
C语言<<超全.超重要>>知识点总结
c语言·开发语言
MeowKnight95810 小时前
【C】使用C语言举例说明逻辑运算符的短路特性
c语言·1024程序员节
2401_8582861111 小时前
OS36.【Linux】简单理解EXT2文件系统(2)
linux·运维·服务器·数据结构·文件系统·ext2
小羊学伽瓦11 小时前
【Java数据结构】——常见力扣题综合
java·数据结构·leetcode·1024程序员节