数据结构 --- 顺序表

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

顺序表分为:静态顺序表、动态顺序表

一.静态顺序表

cpp 复制代码
#define N 7
typedef int SLDataType;

typedef struct Seqlist
{
	SLDataType array[N];  //定长数组
	size_t size;          //有效元素个数
}Seqlist;

静态顺序表突出的特点是:少了不够、多了浪费。

二.动态顺序表

cpp 复制代码
typedef int SLDataType;

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

动态顺序表突出的特点是:按需申请。

三.实现增删查改

基于顺序表的核心思想,参考之前的通讯录程序,我们来用顺序表实现增删查改。

1.准备工作:

欲实现增删查改,首先我们需要分装函数:

|--------|-----------|-----------|
| test.c | SeqList.c | SeqList.h |
| 测试模块 | 函数模块 | 声明模块 |

2.声明模块(SeqList.h):

cpp 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int SLDataType;
#define INIT_CAPACITY 4

typedef struct SeqList
{
	SLDataType* a;
	int size;
	int capacity;
}SL;

void SLInit(SL* ps);
void SLDestroy(SL* ps);
void SLPrint(SL* ps);
void SLCheckCapacity(SL* ps);
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
int SLFind(SL* ps, SLDataType x);

这段代码定义了一个动态顺序表(动态数组)的接口:

一、结构定义解析

cpp 复制代码
typedef int SLDataType;
#define INIT_CAPACITY 4

typedef struct SeqList
{
    SLDataType* a;      // 动态数组指针
    int size;           // 当前元素个数
    int capacity;       // 当前容量
} SL;
关键成员
  1. a
    • 指向动态分配的数组,存储顺序表元素。
  2. size
    • 当前有效元素个数(范围:0 ≤ size ≤ capacity)。
  3. capacity
    • 数组当前的最大容量,扩容时会动态调整。

二、接口功能分析

1. 生命周期管理
  • SLInit(SL* ps):初始化顺序表。
  • SLDestroy(SL* ps):释放内存,销毁顺序表。
2. 元素操作
  • 尾部操作
    SLPushBack(尾插)、SLPopBack(尾删)。
  • 头部操作
    SLPushFront(头插)、SLPopFront(头删)。
  • 任意位置操作
    SLInsert(指定位置插入)、SLErase(指定位置删除)。
3. 辅助功能
  • SLPrint:打印顺序表元素。
  • SLCheckCapacity:检查容量并在需要时扩容。
  • SLFind:查找元素并返回位置。

三、性能分析

操作 时间复杂度 说明
尾插 PushBack O(1) 均摊复杂度(扩容时 O (n))
头插 PushFront O(n) 需要移动所有元素
指定位置插入 SLInsert O(n) 平均移动 n/2 个元素
尾删 PopBack O(1) 直接减少 size,无需移动元素
头删 PopFront O(n) 需要移动所有元素
指定位置删除 SLErase O(n) 平均移动 n/2 个元素
查找 Find O(n) 需遍历数组

3.函数模块(SeqList.c):

确定了具体函数以后,接下来的任务就是如何将这些函数在.c文件中逐个实现:

<1>.初始化函数:

cpp 复制代码
void SLInit(SL* ps)
{
	assert(ps);
	ps->a = (SLDataType*)malloc(sizeof(SLDataType)* INIT_CAPACITY);
	if (ps->a == NULL)
	{
		perror("malloc fail");
		return;
	}

	ps->size = 0;
	ps->capacity = INIT_CAPACITY;
}
一、功能概述

SLInit 函数的核心目的是:

  1. 为顺序表分配初始内存空间(容量为 INIT_CAPACITY,通常为 4)。
  2. 初始化顺序表的状态(sizecapacity)。
  3. 处理内存分配失败的情况。
二、代码逻辑拆解
cpp 复制代码
void SLInit(SL* ps)
{
    assert(ps);  // 检查指针有效性
    
    // 分配初始内存
    ps->a = (SLDataType*)malloc(sizeof(SLDataType)* INIT_CAPACITY);
    
    // 处理内存分配失败
    if (ps->a == NULL)
    {
        perror("malloc fail");
        return;
    }

    // 初始化状态
    ps->size = 0;
    ps->capacity = INIT_CAPACITY;
}
三、关键技术细节
指针有效性检查
cpp 复制代码
assert(ps);
  • 使用 assert 确保传入的指针非空。
  • 注意assert 在调试模式下生效,发布版本可能被忽略。建议结合运行时检查(如 if (ps == NULL))增强健壮性。
内存分配
cpp 复制代码
ps->a = (SLDataType*)malloc(sizeof(SLDataType)* INIT_CAPACITY);
  • 分配 INIT_CAPACITY 个元素的空间。
  • 强制类型转换 :在 C 语言中,malloc 返回 void*,可隐式转换为其他指针类型,因此 (SLDataType*) 可省略。但在 C++ 中必须显式转换。
错误处理
cpp 复制代码
if (ps->a == NULL)
{
    perror("malloc fail");
    return;
}
  • 当内存分配失败时:
    1. 使用 perror 输出错误信息(包含系统错误描述)。
    2. 函数直接返回,此时 ps->aNULL,后续操作需检查指针有效性。
  • 潜在风险 :调用者可能未检查 a 是否为 NULL,导致后续操作崩溃。
状态初始化
cpp 复制代码
ps->size = 0;
ps->capacity = INIT_CAPACITY;
  • size 初始化为 0,表示顺序表为空。
  • capacity 记录当前分配的空间大小。

<2>.尾插函数:

cpp 复制代码
void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);
	//扩容:
	if (ps->size == ps->capacity)
{
   
	SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
	if (tmp == NULL)
	{
		perror("realloc fail");
		return;
	}
	ps->a = tmp; 
	ps->capacity *= 2;
}

	ps->a[ps->size++] = x;
}
一、功能概述

SLPushBack 函数的核心目的是:

  1. 将元素 x 添加到顺序表的尾部。
  2. 若顺序表已满(size == capacity),则自动扩容(容量翻倍)。
  3. 处理内存分配失败的情况。
二、代码逻辑拆解
cpp 复制代码
void SLPushBack(SL* ps, SLDataType x)
{
    assert(ps);  // 检查指针有效性
    
    // 扩容逻辑
    if (ps->size == ps->capacity)
    {
        SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
        if (tmp == NULL)
        {
            perror("realloc fail");
            return;
        }
        ps->a = tmp; 
        ps->capacity *= 2;
    }

    // 插入元素
    ps->a[ps->size++] = x;
}
三、关键技术细节
指针有效性检查
cpp 复制代码
assert(ps);
  • 使用 assert 确保传入的指针非空。
  • 注意assert 在调试模式下生效,发布版本可能被忽略。建议结合运行时检查(如 if (ps == NULL))增强健壮性。
动态扩容机制
cpp 复制代码
if (ps->size == ps->capacity)
{
    SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
    // ...
}
  • 扩容条件 :当 size 等于 capacity 时触发。
  • 扩容策略 :容量翻倍(ps->capacity * 2),均摊时间复杂度为 O (1)。
  • 安全使用 realloc
    1. 使用临时指针 tmp 接收 realloc 的返回值,避免内存泄漏(若 realloc 失败,原指针 ps->a 保持不变)。
    2. 强制类型转换:在 C 语言中可省略,但 C++ 中必须显式转换。
错误处理
cpp 复制代码
if (tmp == NULL)
{
    perror("realloc fail");
    return;
}
  • 当内存分配失败时:
    1. 使用 perror 输出错误信息。
    2. 函数直接返回,此时原指针 ps->a 未被修改,但插入操作失败。
  • 潜在风险:调用者可能未检查操作是否成功,误认为元素已插入。
元素插入
cpp 复制代码
ps->a[ps->size++] = x;
  • 将元素 x 放入当前 size 位置,然后 size 自增。

  • 等价于

    cpp 复制代码
    ps->a[ps->size] = x;
    ps->size++;

<3>.头插函数:

cpp 复制代码
void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);
	if (ps->size == ps->capacity)
{
	SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
	if (tmp == NULL)
	{
		perror("realloc fail");
		return;
	}
	ps->a = tmp; 
	ps->capacity *= 2;
}

	int end = ps->size - 1;
	while (end >= 0)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	ps->a[0] = x;
	ps->size++;

}
一、功能概述

SLPushFront 函数的核心目的是:

  1. 将元素 x 添加到顺序表的头部。
  2. 若顺序表已满(size == capacity),则自动扩容(容量翻倍)。
  3. 通过元素后移实现头部插入。
二、代码逻辑拆解
cpp 复制代码
void SLPushFront(SL* ps, SLDataType x)
{
    assert(ps);  // 检查指针有效性
    
    // 扩容逻辑(同SLPushBack)
    if (ps->size == ps->capacity)
    {
        SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
        if (tmp == NULL)
        {
            perror("realloc fail");
            return;
        }
        ps->a = tmp; 
        ps->capacity *= 2;
    }

    // 元素后移
    int end = ps->size - 1;
    while (end >= 0)
    {
        ps->a[end + 1] = ps->a[end];
        --end;
    }
    
    // 插入元素
    ps->a[0] = x;
    ps->size++;
}
三、关键技术细节
扩容机制

SLPushBack 相同,当容量不足时进行翻倍扩容,确保有空间插入新元素。

元素后移
cpp 复制代码
int end = ps->size - 1;
while (end >= 0)
{
    ps->a[end + 1] = ps->a[end];
    --end;
}
  • 移动方向:从最后一个元素开始,依次向后移动一位,直到所有元素后移完毕。
  • 时间复杂度 :O (n),需移动全部 size 个元素。
插入操作
cpp 复制代码
ps->a[0] = x;
ps->size++;
  • 将新元素放入数组头部(索引 0),并更新 size

<4>.扩容函数:

观察发现,插入函数需要扩容,为减轻冗长代码,我们可以将扩容代码另外分装:

cpp 复制代码
void SLCheckCapacity(SL* ps)
{
	assert(ps);
	//扩容:
	if (ps->size == ps->capacity)
	{
		SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		ps->a = tmp; 
		ps->capacity *= 2;
	}
}

<5>.尾删函数:

cpp 复制代码
void SLPopBack(SL* ps)
{
    assert(ps);  // 检查指针有效性
    
    // 处理空表情况
    if (ps->size == 0)
        return;
    
    // 逻辑删除:减小size,无需清除元素
    ps->size--;
}

尾删函数较为简单,不加以赘述......

<6>.头删函数:

cpp 复制代码
void SLPopFront(SL* ps)
{
	assert(ps);
	assert(ps->size > 0);

	int begin = 1;
	while (begin < ps->size)
	{
		ps->a[begin - 1] = ps->a[begin];
		++begin;
	}

	ps->size--;
}
一、功能概述

SLPopFront 函数的核心目的是:

  1. 删除顺序表的第一个元素。
  2. 通过元素前移覆盖原头部元素。
  3. 减小 size 以标记有效元素数量减少。
二、代码逻辑拆解
cpp 复制代码
void SLPopFront(SL* ps)
{
    assert(ps);  // 检查指针有效性
    assert(ps->size > 0);  // 确保表非空

    // 元素前移覆盖头部
    int begin = 1;
    while (begin < ps->size)
    {
        ps->a[begin - 1] = ps->a[begin];
        ++begin;
    }

    // 减小size
    ps->size--;
}
三、关键技术细节
断言检查
cpp 复制代码
assert(ps);
assert(ps->size > 0);
  • 确保传入指针非空且表非空。
  • 注意assert 仅在调试模式生效,发布版本可能忽略,需依赖调用者检查。
元素前移
cpp 复制代码
int begin = 1;
while (begin < ps->size)
{
    ps->a[begin - 1] = ps->a[begin];
    ++begin;
}
  • 移动方向:从第二个元素(索引 1)开始,依次向前覆盖前一个元素。
  • 时间复杂度 :O (n),需移动全部 size-1 个元素。
逻辑删除
cpp 复制代码
ps->size--;
  • 通过减小 size 标记有效元素减少,无需物理清除最后一个元素。

<7>.指定位置插入函数:

cpp 复制代码
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);

	SLCheckCapacity(ps);

	int end = ps->size - 1;
	while (end >= pos)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	ps->a[pos] = x;
	ps->size++;
}
一、功能概述

SLInsert 函数的核心目的是:

  1. 将元素 x 插入到顺序表的指定位置 pos
  2. 若顺序表已满,自动扩容。
  3. 通过元素后移为新元素腾出空间。
二、代码逻辑拆解
cpp 复制代码
void SLInsert(SL* ps, int pos, SLDataType x)
{
    assert(ps);  // 检查指针有效性
    assert(pos >= 0 && pos <= ps->size);  // 确保pos合法

    SLCheckCapacity(ps);  // 检查并扩容(若需要)

    // 元素后移
    int end = ps->size - 1;
    while (end >= pos)
    {
        ps->a[end + 1] = ps->a[end];
        --end;
    }
    
    // 插入元素
    ps->a[pos] = x;
    ps->size++;
}
三、关键技术细节
合法性检查
cpp 复制代码
assert(pos >= 0 && pos <= ps->size);
  • 合法范围0 ≤ pos ≤ size
    • pos == size 时,等价于尾部插入。
    • pos > size,会导致元素间出现空洞(如 [1, 2, pos=5插入3] → [1, 2, ?, ?, ?, 3])。
动态扩容
cpp 复制代码
SLCheckCapacity(ps);
  • 调用扩容函数,确保有足够空间插入新元素。
  • 扩容策略:通常为容量翻倍(如从 4→8→16),均摊时间复杂度 O (1)。
元素后移
cpp 复制代码
int end = ps->size - 1;
while (end >= pos)
{
    ps->a[end + 1] = ps->a[end];
    --end;
}
  • 移动方向 :从最后一个元素开始,逐个后移直到 pos 位置。
  • 时间复杂度:O (n)(最坏情况:插入头部需移动所有元素)。
插入操作
cpp 复制代码
ps->a[pos] = x;
ps->size++;
  • 将元素放入指定位置 pos,更新 size

<8>.指定位置删除函数:

cpp 复制代码
void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);

	int begin = pos + 1;
	while (begin < ps->size)
	{
		ps->a[begin - 1] = ps->a[begin];
		++begin;
	}
	ps->size--;
}
一、功能概述

SLErase 函数的核心目的是:

  1. 删除顺序表中指定位置 pos 的元素。
  2. 通过元素前移覆盖被删除元素。
  3. 减小 size 以标记有效元素数量减少。
二、代码逻辑拆解
cpp 复制代码
void SLErase(SL* ps, int pos)
{
    assert(ps);  // 检查指针有效性
    assert(pos >= 0 && pos < ps->size);  // 确保pos合法

    // 元素前移覆盖
    int begin = pos + 1;
    while (begin < ps->size)
    {
        ps->a[begin - 1] = ps->a[begin];
        ++begin;
    }

    // 减小size
    ps->size--;
}
三、关键技术细节
合法性检查
cpp 复制代码
assert(pos >= 0 && pos < ps->size);
  • 合法范围0 ≤ pos < size
    • pos == size,会导致访问越界(如 size=3 时,pos=3 访问 a[4])。
元素前移
cpp 复制代码
int begin = pos + 1;
while (begin < ps->size)
{
    ps->a[begin - 1] = ps->a[begin];
    ++begin;
}
  • 移动方向 :从 pos 的下一个位置开始,依次向前覆盖前一个元素。
  • 时间复杂度:O (n)(最坏情况:删除头部需移动所有元素)。
逻辑删除
cpp 复制代码
ps->size--;
  • 通过减小 size 标记有效元素减少,无需物理清除最后一个元素。

基于以上的两个指定位置函数,我们可以进一步简化代码,将头插、尾插、头删、尾删函数均用任意位置函数代替,代码量将大大减少。后续总体展示时可以看出。

<9>.查找函数:

cpp 复制代码
int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; ++i)
	{
		if (ps->a[i] == x)
		{
			return i;
		}
	}
	return -1;
}

代码较为简单,不加以赘述.....

综上,整个程序基本完成,接下来将总体展示,包括三个模块以及一些代码的简化:

4.整体展示:

<1>.测试模块(test.c):

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include"SeqList.h"

void TestSeqList1()
{
	SL s;
	SLInit(&s);
	SLPushBack(&s, 1);
	SLPushBack(&s, 2);
	SLPushBack(&s, 3);
	SLPushBack(&s, 4);
	SLPushBack(&s, 5);
	SLPushBack(&s, 6);
	SLPrint(&s);
	SLPopBack(&s);
	SLPopBack(&s);
	SLPrint(&s);
	SLPopFront(&s);
	SLPrint(&s);
	SLPopFront(&s);
	SLPrint(&s);
	SLInsert(&s, 2, 1);
	SLPrint(&s);

	void* ptr1 = malloc(10);
	printf("%p\n", ptr1);
	void* ptr2 = realloc(ptr1,20);
	printf("%p\n", ptr2);         //两次开辟地址不同,所以是异地扩容


	SLDestroy(&s);
}

int main()
{
	TestSeqList1();

	return 0;
}

当然,若想要设置菜单,和通讯录一样也是可以的......

<2>.函数模块(SeqList.c):

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"

void SLInit(SL* ps)
{
	assert(ps);
	ps->a = (SLDataType*)malloc(sizeof(SLDataType)* INIT_CAPACITY);
	if (ps->a == NULL)
	{
		perror("malloc fail");
		return;
	}

	ps->size = 0;
	ps->capacity = INIT_CAPACITY;
}

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

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

void SLCheckCapacity(SL* ps)
{
	assert(ps);
	//扩容:
	if (ps->size == ps->capacity)
	{
		SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		ps->a = tmp; 
		ps->capacity *= 2;
	}
}

void SLPushBack(SL* ps, SLDataType x)
{
	//assert(ps);
	扩容:
	//SLCheckCapacity(ps);

	//ps->a[ps->size++] = x;
	SLInsert(ps, ps->size, x);
}

void SLPopBack(SL* ps)
{
	/*assert(ps);
	if (ps->size == 0)
		return;
	ps->size--;*/

	SLErase(ps, ps->size - 1);
}

void SLPushFront(SL* ps, SLDataType x)
{
	/*assert(ps);
	SLCheckCapacity(ps);

	int end = ps->size - 1;
	while (end >= 0)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	ps->a[0] = x;
	ps->size++;*/

	SLInsert(ps, 0, x);
}

void SLPopFront(SL* ps)
{
	/*assert(ps);
	assert(ps->size > 0);

	int begin = 1;
	while (begin < ps->size)
	{
		ps->a[begin - 1] = ps->a[begin];
		++begin;
	}

	ps->size--;*/

	SLErase(ps, 0);
}

void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);

	SLCheckCapacity(ps);

	int end = ps->size - 1;
	while (end >= pos)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	ps->a[pos] = x;
	ps->size++;
}

void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);

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

int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; ++i)
	{
		if (ps->a[i] == x)
		{
			return i;
		}
	}
	return -1;
}

<3>.声明模块(SeqList.h):

cpp 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int SLDataType;
#define INIT_CAPACITY 4

typedef struct SeqList
{
	SLDataType* a;
	int size;
	int capacity;
}SL;

void SLInit(SL* ps);
void SLDestroy(SL* ps);
void SLPrint(SL* ps);
void SLCheckCapacity(SL* ps);
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
int SLFind(SL* ps, SLDataType x);

至此,代码全部结束,欢迎指出错误与不当之处,谢谢!!!

相关推荐
✿ ༺ ོIT技术༻4 分钟前
笔试强训:Day6
数据结构·c++·算法
jz_ddk3 小时前
[学习] C语言多维指针探讨(代码示例)
linux·c语言·开发语言·学习·算法
星夜9825 小时前
C++回顾 Day6
开发语言·数据结构·c++·算法
艾莉丝努力练剑7 小时前
深入详解编译与链接:翻译环境和运行环境,翻译环境:预编译+编译+汇编+链接,运行环境
c语言·开发语言·汇编·学习
xwxh8 小时前
C 语言基础五: 数组 - 练习demo
c语言
Yurko1310 小时前
【C语言】函数指针及其应用
c语言·开发语言·学习
熙曦Sakura10 小时前
【MySQL】C语言连接
c语言·mysql·adb
woho77889911 小时前
伊吖学C笔记(4、循环、自定义函数、二级菜单)
c语言·开发语言·笔记
算法歌者11 小时前
[C]基础16.数据在内存中的存储
c语言
Tony__Ferguson11 小时前
数据结构——优先级队列(PriorityQueue)
android·java·数据结构