数据结构 - 顺序表

一. 线性表的概念

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

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

外传:

在数组实现的线性表中,所有元素都连续存储在内存的一段固定区域内,可以通过索引直接访问每个元素。这种实现方式的优点是访问速度快,但缺点是大小固定,不够灵活。

在链表实现的线性表中,元素可以分散存储在内存中,每个元素通过指针或引用与其前驱和后继元素相连接。这种实现方式的优点是可以动态调整大小,更加灵活,但缺点是访问某个特定元素时可能需要从头开始遍历,访问速度较慢。

二. 顺序表介绍

2.1 顺序表的概念

顺序表是一种线性表的实现方式,它使用连续的内存空间来存储表中的元素,使得每个元素都可以通过数组索引快速访问。这种数据结构非常类似于数组,事实上,在很多编程语言中,顺序表就是以数组的形式实现的。那顺序表和数组有区别吗?

顺序表:顺序表是用 一段物理地址连续存储单元 ,依次存储数据元素的线性结构(顺序表就是将数据连着存放,存放的数据空间也是连着的)

数组:数据不用按顺序的存放,可以随意的存放数组不按空间顺序随意地存放数据,但是顺序表的数据是要按照空间的的顺序存放)

 2.2 顺序表的结构

1. 静态顺序表

静态顺序表使用固定大小的数组来存储数据,这意味着在创建顺序表时,必须指定一个最大容量,之后这个容量不能改变。因此,静态顺序表的大小在其生命周期内保持不变。

特点

  • 固定容量:一旦确定,容量不能增加或减少。
  • 内存连续性:所有元素都存储在连续的内存位置,这样可以通过索引快速访问任何元素。
  • 高效的访问速度:由于直接通过数组索引访问,访问速度非常快。
  • 内存浪费:如果不使用全部容量,内存资源可能会被浪费。反之,如果需要的元素超出了初始设定的容量,无法存储更多的数据,这限制了其使用的灵活性。

静态顺序表适用于数据量已知且数据量不会改变的情况,如在嵌入式系统或有严格内存管理需求的应用中非常有用。

1. 动态顺序表 

动态顺序表则更为灵活,它使用一个可以在运行时根据需要自动扩容的数组来存储元素。这意味着初始时,顺序表可能具有一个较小的容量,但当添加的元素超过当前容量时,顺序表可以动态地分配一个更大的数组,将旧元素复制到新的数组中,并释放旧数组。

特点

  • 动态扩容:当需要更多空间时,可以自动扩展其容量。
  • 内存连续性:尽管动态顺序表可以扩容,但其元素仍然存储在连续的内存位置。
  • 灵活性高:适应不同的数据存储需求,尤其是数据量不确定的场合。
  • 可能的高扩容成本:动态扩容涉及到复制整个数组到新的内存地址,这可能导致较高的运行时开销,特别是数组很大时。

我们会发现,动态顺序表的结构体比静态顺序表的结构体多了一个 capacity 成员。

我们用这个成员记录当前顺序表的最大容量,当有效数据个数 size 和 capacity 相等时,说明了当

前顺序表空间已满,需要进行扩容。

三. 动态顺序表的实现

首先建立三个文件:

SeqList.h文件,用于函数声明

SeqList.c文件,用于函数的定义

Test.c文件,用于测试函数

建立三个文件的目的: 将顺序表作为一个项目来进行书写,方便我们的学习与观察。

3.1 定义结构体(SL)

// 定义SLDateType为int类型,这是顺序表中将要存储的数据类型
typedef int SLDateType;// 定义一个结构体,用来表示顺序表
typedef struct SeqList
{SLDateType* a; // 声明了一个指向动态分配数组的指针,这个数组用于存储顺序表中的元素int size;      // 记录当前顺序表中已经存储的元素的数量,即顺序表的当前大小int capacity;  // 记录顺序表分配的存储容量,即数组的最大可能大小
} SeqList;

外传:

在C语言中使用 typedef 来定义新的数据类型名有多个原因,尤其是在实现如顺序表这样的数据结构时。SLDateType 在顺序表定义中的使用有以下几个主要好处:

1. 抽象数据类型

通过定义 SLDateType 作为数据类型,可以轻松地抽象和封装数据结构中元素的具体类型。这种抽象化允许程序设计者更改数据的基础类型而不影响使用该数据结构的代码。例如,如果将来需要将顺序表中的数据从整数 (int) 改为浮点数 (double) 或其他类型,只需更改 SLDateType 的定义,而不需修改顺序表的每个实现细节。

2. 代码可维护性

当你将数据类型定义为 SLDateType 而不是直接使用 int 或其他具体类型时,代码的可读性和可维护性提高了。它在代码中创建了一个明确的表示点,表明所有使用 SLDateType 的地方都是处理相同类型的数据,使得数据类型的更改集中化和简化。

3. 便于代码重用

如果你有多个数据结构或算法需要处理相同类型的数据,使用 SLDateType 可以确保这些组件可以更容易地共享和重用。例如,可以为不同的数据结构(如链表、堆、栈等)定义通用的操作和函数,而不必为每种类型写重复的代码。

4. 易于进行类型检查和修改

在大型项目或需要严格类型检查的项目中,使用 typedef 可以更容易地管理类型安全。更改底层数据类型时,通过查看 SLDateType 的使用情况,可以很容易地识别出可能受影响的代码部分。

5. 兼容性与扩展性

在开发库或框架时,使用 typedef 为数据类型命名提供了额外的兼容性和扩展性。如果库的用户需要将库与其它代码或第三方库整合,他们可以通过修改 typedef 定义来实现类型匹配,而不是修改库本身的代码。

3.2 初始化结构体(SLInit)

初始化一个顺序表,存在一个关键的问题,那就是是否写成了参数传递的方式。如果使用的传参的方式传递参数,这意味着函数内部的修改仅影响原始的顺序表对象的拷贝。这样的初始化实际上是无效的,因为调用这个函数后,原始的顺序表不会被初始化。

为了让初始化生效,需要通过指针来传递顺序表,这样在函数内部对顺序表的修改才会影响到原始的对象。

// 初始化顺序表
// 参数 ps 是指向顺序表的指针
void SLInit(SL* ps)
{assert(ps); // 断言:确保传入的指针ps非空,防止解引用空指针造成程序崩溃ps->a = NULL;        // 将数组指针设置为NULL,表示当前没有分配任何内存给数据ps->size = 0;        // 将顺序表的当前元素数量设置为0,表示顺序表为空ps->capacity = 0;    // 将顺序表的容量设置为0,表示还没有分配任何容量
}

外传:

如果顺序表没有进行适当的初始化,就会出现一系列潜在的问题,这些问题可能会严重影响程序的运行。在C语言中,未初始化的变量特别是指向动态内存的指针,以及相关的整型变量(如数组的大小和容量)可以有不确定的初始值,这些都会引起以下问题:

1. 不确定的指针值

如果指向数组的指针没有初始化,它可能指向任意的内存地址。尝试通过这个未初始化的指针访问或修改数据会导致未定义行为,这通常包括:

  • 程序崩溃:访问无效或不属于程序的内存地址通常会导致程序异常终止。
  • 数据损坏:如果指针偶然指向某些敏感区域,通过该指针的写操作可能会破坏其他数据。
  • 安全漏洞:未初始化的指针被恶意利用可能会导致安全漏洞,例如缓冲区溢出攻击。

2. 不确定的数组大小和容量

如果顺序表中的sizecapacity没有初始化,它们的值将是不确定的。这会导致一系列问题,如:

  • 越界访问:如果sizecapacity不小心设置为一个非常大的数,程序可能会尝试访问数组的不存在的部分,这同样会导致程序崩溃。
  • 内存分配错误:如果capacity的值是随机的,当尝试基于这个值分配内存时,可能因为请求了过多的内存而失败。
  • 错误的行为:基于错误的size值,顺序表的行为可能不正确,例如认为它已经满了或还有大量空间。

3.3 检查顺序表是否需要扩容(SLCheckCapacity)   

扩容是一个常见且有用的特性,可以在运行时根据数据量的变化调整内存使用。这里是几个关键点来说明何时以及为什么顺序表需要扩容:

1. 容量达到极限

当顺序表的元素数量达到其当前容量的极限时,再添加新元素就无法进行了,除非扩展其容量。在这种情况下,扩容是必要的,以避免数据丢失或程序错误。

2. 性能优化

在顺序表接近满容量时,即使还可以添加少量元素,执行扩容也是一种常见的做法。这样做可以减少将来可能的多次扩容操作所需的总成本,因为频繁的小扩容操作(例如每次只增加一个元素的空间)会导致多次昂贵的内存复制操作。

扩容机制的实现

扩容通常涉及以下步骤:

  • 确定新容量:新容量通常是当前容量的两倍,这种加倍策略是为了平衡扩容的成本和频率(称为幅度扩展)。
  • 分配新数组:创建一个新的、更大的数组来存储元素。
  • 复制元素:将现有元素从旧数组复制到新数组。
  • 释放旧数组:释放旧数组占用的内存空间。
  • 更新指针和容量值:将顺序表的数据指针指向新数组,并更新容量值。

在扩容是,会需要用到动态分配函数 realloc ()函数 ,如果对这个函数不太了解,可以看一下这篇文章:动态分配函数

void SLCheckCapacity(SL* ps)
{assert(ps); // 断言,确保传入的顺序表指针非空if (ps->size == ps->capacity){ // 如果当前元素数量等于容量,需要扩容int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;  // 新容量计算策略:如果当前容量为0,则初始为4;否则容量翻倍SLDateType* temp = (SLDateType*)realloc(ps->a, newCapacity * sizeof(SLDateType)); // 尝试重新动态分配内存if (temp == NULL){ // 检查realloc是否成功,注意这里是比较操作,应使用两个等号==perror("realloc failed"); // 如果内存分配失败,输出错误消息exit(-1);  // 退出程序}ps->a = temp;  // 更新顺序表的数据指针ps->capacity = newCapacity;  // 更新顺序表的容量}
}

3.4 尾插(SLPushBack)

尾插操作是指在顺序表(或类似的数据结构如动态数组、列表等)的末端插入一个新元素。

void SLPushBack(SL* ps, SLDateType x) {assert(ps); // 断言,确保传入的顺序表指针非空,避免解引用空指针导致程序崩溃SLCheckCapacity(ps); // 检查顺序表的容量是否足够,如果不足则进行扩容ps->a[ps->size] = x; // 将新元素x存放在数组的当前末尾位置,ps->size是指向下一个空闲位置的索引 
//这里为什么用size而不用size-1或者别的呢,因为size的大小,就是数组最后一个数+1的下标ps->size++; // 更新顺序表的元素计数,增加1,以反映添加了新元素
}

3.5 尾删(SLPopBack)

尾删操作是指在顺序表的尾部删除数据。

void SLPopBack(SL* ps) {assert(ps); // 断言,确保传入的顺序表指针非空,避免解引用空指针导致程序崩溃//ps->a[ps->size - 1] = 0;  //用于清除顺序表最后一个元素的数据,将其设置为 0。// 温和的错误处理方式: 检查顺序表是否为空,如果为空则打印信息并退出函数/*if (ps->size == 0) {printf("顺序表已空\n");return;}*/// 暴力的错误处理方式: 使用断言确保顺序表不为空,如果为空程序在调试模式下会中断执行assert(ps->size > 0); // 确保顺序表中至少有一个元素,防止下溢// 可选的清除操作: 清除顺序表最后一个元素的数据,将其设置为 0// ps->a[ps->size - 1] = 0;ps->size--; // 减少顺序表的大小,逻辑上移除最后一个元素
}

注意:当顺序表没有元素时,也就是 sz 为 0 时,不可以进行删除

举例: 假如顺序表有 5 个数据,此时需要尾删 6 个数据,那么这时,顺序表的 size = -1。此时程序不会报错,因为本身就没有数据可以删除,但是让我们在进行尾插的时候,程序就会报错了,因为尾插数据会访问 size 的下表 -1 此时出现了越界访问,程序出错。

解决方案 :加入断言进行判断,防止size 的下标越界 

3.6 头插(SLPushFront)

头插操作是指在最前端插入一个新元素。这意味着将所有现有元素向后移动一个位置以空出第一个位置,然后将新元素放置在顺序表的起始位置。头插操作相较于尾插操作,通常更为耗时,因为它涉及到移动更多的元素。

当你想进行挪动数据的时候,现在给你有两个选择,你会选择哪个呢?

  • 从第一个数据进行从先往后的挪动
  • 从最后一个数据进行从后往前的挪动

// 在顺序表的开始位置插入一个新元素
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++;  // 更新顺序表的大小
}

由于头插需要移动大量元素,因此在元素较多时,头插操作的效率比较低。如果频繁进行头插操作,可能需要考虑使用其他数据结构,如链表,这样可以更高效地在头部添加元素。 

3.7 头删(SLPopFront)

头删原理:头删就是将下标为 0 的数据删除,其余数据依次向前挪动,此时同样出现两情况。

当你想进行挪动数据的时候,现在给你有两个选择,你又会选择哪个呢?

  • 先从下标为 1 的数据向前挪动
  • 先从最后一个数据向前挪动

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--;  // 减少顺序表的大小,逻辑上移除最后一个空出的位置的元素
}

3.8 在指定位置插入数据(SLInsert)

在顺序表中在指定位置插入数据时,需要注意以下几个要点:

  1. 位置有效性:首先要确认指定的插入位置是否有效。有效位置一般是指从0到当前元素个数(n)的位置(假设数组从0开始索引),即在数组的开始位置到最后一个元素之后的直接位置。

  2. 空间容量:确保顺序表的存储空间足够,如果插入新元素导致元素总数超过当前数组容量,需要进行扩容。这通常涉及分配一个更大的数组空间,将旧数组中的数据复制到新数组中,然后再插入新元素。

  3. 数据移动:与头插类似,指定位置插入也需要移动数据。具体而言,从插入点开始的所有后续元素需要向后移动一个位置,以空出位置来插入新的元素。例如,如果在位置i插入新元素,那么原位置i及之后的所有元素都要向后移动。

  4. 更新长度:插入新元素后,需要更新顺序表的长度信息,确保它反映了当前元素的正确数量。

  5. 效率考虑:在指定位置插入的效率取决于插入位置。如果插入位置接近数组的末端,移动的元素较少,效率较高;如果插入位置接近开始位置,可能需要移动大量元素,效率较低。因此,插入操作的平均时间复杂度为O(n),其中n为元素数量。

  6. 边界条件处理:实现时要特别注意处理边界条件,包括处理数组边界以防止索引越界,以及在数组已满时进行正确的扩容操作。

// 函数定义:在顺序表ps的指定位置pos插入一个新元素x
void SLInsert(SL* ps, int pos, SLDateType x)
{assert(ps); // 断言:确保传入的顺序表指针ps非空,防止空指针引用导致的程序崩溃assert(ps->size > 0); // 断言:确保顺序表的大小至少为1,这个断言可能不总是必要,特别是如果允许在空表中插入assert(pos <= ps->size); // 断言:确保插入位置有效,插入位置可以在数组的最后一个元素之后(即等于size)SLCheckCapacity(ps); // 检查顺序表的容量,如果不够则进行扩容操作,保证有空间插入新元素int end = ps->size - 1; // 获取顺序表中最后一个元素的位置// 循环移动元素,为新元素腾出位置while (end >= pos){ps->a[end + 1] = ps->a[end]; // 将元素向后移动一个位置end--; // 更新处理的元素位置}ps->a[pos] = x; // 在指定位置pos放入新元素xps->size++; // 更新顺序表的元素数量
}

同样也要考虑是从前往后删还是从后往前删,需要一个end下标,数据从后往前依次后挪,直到pos下标移动完毕。

拓展:

该功能其实也可以实现头插和尾插,所以我们可以在头插和尾插中复用该功能

// 函数定义:在顺序表的开始位置进行头插操作
void SeqListPushFront(SL* ps, SLDataType x)
{SeqListInsert(ps, 0, x); // 调用通用的插入函数,将新元素x插入到顺序表的第一个位置(索引0)
}// 函数定义:在顺序表的末尾进行尾插操作
void SeqListPushBack(SL* ps, SLDataType x)
{SeqListInsert(ps, ps->sz, x); // 调用通用的插入函数,将新元素x插入到顺序表的末尾// 这里ps->sz是当前顺序表中元素的数量,即新元素的插入位置
}

3.9 在指定位置删除数据(SLErase) 

要实现这一功能,我们需要一个begin下标,数据从前往后依次前挪,直到sz-1下标移动完毕。

在顺序表中删除指定位置的数据时,需要注意以下几个关键点:

  1. 位置有效性:确保指定的位置有效,即该位置索引应该在顺序表的当前元素范围内。通常,有效的索引范围是从0到当前元素个数减一(0size-1)。

  2. 数据移动:删除元素后,需要将指定位置后的所有元素向前移动一个位置来填补被删除元素留下的空位。这样可以保持顺序表的连续性。

  3. 更新长度:完成删除操作后,需要更新顺序表的元素数量,即减少顺序表的size属性。

  4. 效率考虑:删除操作的效率取决于删除点的位置。如果删除位置靠近顺序表的起始位置,则需要移动更多的元素,效率较低;如果靠近末尾,则需要移动的元素较少,效率较高。因此,平均时间复杂度为O(n),其中n为元素数量。

  5. 边界条件处理:实现时要特别注意处理边界条件,比如删除操作前顺序表是否为空,以及处理数组边界以防止索引越界。

void SLErase(SL* ps, int pos)
{assert(ps); // 断言:确保顺序表的指针ps非空,防止空指针引用导致的程序崩溃assert(pos >= 0); // 断言:确保指定的位置pos是有效的,不能是负数assert(pos < ps->size); // 断言:确保指定的位置pos在顺序表的当前元素范围内(即小于元素数量size)assert(ps->size > 0);  // 断言:检查顺序表的元素数量不为零,确保有元素可以删除int begin = pos + 1; // 设置开始移动数据的位置,从pos的下一个位置开始// 循环移动元素,覆盖需要删除的元素while (begin < ps->size){ps->a[begin - 1] = ps->a[begin]; // 将后续元素向前移动一位,填补被删除元素的位置begin++; // 移动到下一个元素}ps->size--; // 删除元素后,顺序表的元素总数减少1
}

同样的,该接口也可复用于头删和尾删中: 

// 头删
void SeqListPopFront(SL* ps)
{SeqListErase(ps, 0);
}// 尾删
void SeqListPopBack(SL* ps)
{SeqListErase(ps, ps->sz - 1);
}

3.10 查找某一个数据的位置(SLFind) 

在顺序表ps中查找元素x的位置

int SLFind(SL* ps, SLDateType x)
{assert(ps); // 断言:确保顺序表的指针ps非空// 遍历顺序表中的所有元素for (int i = 0; i < ps->size; i++){if (ps->a[i] == x) // 如果当前元素等于x{return i; // 返回当前元素的索引}}return -1; // 如果没有找到元素x,返回-1作为失败的标志
}

3.11 查找某一个数据的起始位置(SLFinds)   

在顺序表ps中从索引begin开始查找元素x的位置

int SLFinds(SL* ps, SLDateType x, int begin)
{assert(ps); // 断言:确保顺序表的指针ps非空,防止空指针引用导致的程序崩溃// 从指定的开始位置begin遍历顺序表for (int i = begin; i < ps->size; i++){if (ps->a[i] == x) // 如果当前元素等于x{return i; // 返回当前元素的索引}}return -1; // 如果遍历完毕没有找到元素x,返回-1表示未找到
}

3.12 打印函数(SLPrint)

在每次操作后,可以打印出顺序表,观察操作情况

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

3.13 销毁(SLDestory)

主要目的是销毁顺序表,确保释放与顺序表关联的所有内存资源,防止内存泄漏。

  • 函数首先确认传入的顺序表指针非空,这是为了确保对有效的顺序表进行操作。
  • 如果顺序表的数组指针非空,表明数组已经分配了内存,需要通过free函数释放这部分内存。
  • 释放内存后,数组指针设置为NULL,这是一种良好的编程习惯,可以防止后续代码错误地引用已释放的内存(称为悬挂指针或野指针)。
  • 同时,顺序表的size(元素数量)和capacity(容量)被设置为0,这表明顺序表已经被完全清空并且不再占用任何资源。
void SLDestory(SL* ps)
{assert(ps); // 断言:确保顺序表的指针ps非空,防止空指针引用导致的程序崩溃if (ps->a != NULL) // 检查顺序表的数组指针是否为非空,确保其指向有效的内存区域{free(ps->a); // 使用free函数释放顺序表数组所占用的内存ps->a = NULL; // 将数组指针设置为NULL,避免产生悬挂指针ps->size = ps->capacity = 0; // 将顺序表的大小和容量重置为0,标记顺序表为空和无容量状态}
}

四. 动态顺序表完整代码

 SeqList.h

#pragma once    // 防止重复包含,防止头文件被重复包含多次// 引入必要的库文件
#include <stdlib.h>    // 提供动态内存分配、随机数生成等
#include <string.h>    // 提供字符串操作函数
#include <stdio.h>     // 提供输入输出函数
#include <math.h>      // 提供数学函数
#include <assert.h>    // 提供断言函数#define M 1000         // 预设的某些操作或容量限制的宏定义,具体用途未详细说明// 定义数据类型和顺序表结构体
typedef int SLDateType;   // 数据类型,此处定义为int,可根据实际需要调整
typedef struct Seqlist {SLDateType* a;        // 指向动态分配数组的指针int size;             // 记录存储了多少个有效数据int capacity;         // 空间容量大小
} SL;// 动态顺序表的操作函数声明
void SLInit(SL* ps);                // 初始化顺序表
void SLDestory(SL* ps);             // 销毁顺序表,释放资源
void SLPushBack(SL* ps, SLDateType x);  // 尾插函数,向顺序表末尾添加元素
void SLPrint(SL* ps);               // 打印函数,输出顺序表内容
void SLPopBack(SL* ps);             // 尾删函数,删除顺序表末尾元素
void SLPushFront(SL* ps, SLDateType x); // 头插函数,向顺序表头部添加元素
void SLPopFront(SL* ps);            // 头删函数,删除顺序表头部元素
void SLCheckCapacity(SL* ps);       // 检查是否需要扩容,确保有足够空间添加元素// 中间位置的插入和删除操作
void SLInsert(SL* ps, int pos, SLDateType x);  // 在pos位置插入数据
void SLErase(SL* ps, int pos);                 // 删除pos位置的数据int SLFind(SL* ps, SLDateType x);              // 查找某一个数字的位置
int SLFinds(SL* ps, SLDateType x, int begin);  // 从指定位置开始查找某数字的位置

SeqList.c

#define _CRT_SECURE_NO_WARNINGS 1#include "SeqList.h"void SLInit(SL* ps)
{ps->a = NULL;ps->size = 0;ps->capacity = 0;
}void SLCheckCapacity(SL* ps)
{assert(ps); // 断言,确保传入的顺序表指针非空if (ps->size == ps->capacity){ // 如果当前元素数量等于容量,需要扩容int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;  // 新容量计算策略:如果当前容量为0,则初始为4;否则容量翻倍SLDateType* temp = (SLDateType*)realloc(ps->a, newCapacity * sizeof(SLDateType)); // 尝试重新动态分配内存if (temp == NULL){ // 检查realloc是否成功,注意这里是比较操作,应使用两个等号==perror("realloc failed"); // 如果内存分配失败,输出错误消息exit(-1);  // 退出程序}ps->a = temp;  // 更新顺序表的数据指针ps->capacity = newCapacity;  // 更新顺序表的容量}
}void SLPushBack(SL* ps, SLDateType x) {assert(ps); // 断言,确保传入的顺序表指针非空,避免解引用空指针导致程序崩溃SLCheckCapacity(ps); // 检查顺序表的容量是否足够,如果不足则进行扩容ps->a[ps->size] = x; // 将新元素x存放在数组的当前末尾位置,ps->size是指向下一个空闲位置的索引 //这里为什么用size而不用size-1或者别的呢,因为size的大小,就是数组最后一个数+1的下标ps->size++; // 更新顺序表的元素计数,增加1,以反映添加了新元素
}void SLPopBack(SL* ps) {assert(ps); // 断言,确保传入的顺序表指针非空,避免解引用空指针导致程序崩溃//ps->a[ps->size - 1] = 0;  //用于清除顺序表最后一个元素的数据,将其设置为 0。// 温和的错误处理方式: 检查顺序表是否为空,如果为空则打印信息并退出函数/*if (ps->size == 0) {printf("顺序表已空\n");return;}*/// 暴力的错误处理方式: 使用断言确保顺序表不为空,如果为空程序在调试模式下会中断执行assert(ps->size > 0); // 确保顺序表中至少有一个元素,防止下溢// 可选的清除操作: 清除顺序表最后一个元素的数据,将其设置为 0// ps->a[ps->size - 1] = 0;ps->size--; // 减少顺序表的大小,逻辑上移除最后一个元素
}// 在顺序表的开始位置插入一个新元素
void SLPushFront(SL* ps, SLDateType 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++;  // 更新顺序表的大小
}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--;  // 减少顺序表的大小,逻辑上移除最后一个空出的位置的元素
}void SLInsert(SL* ps, int pos, SLDateType x)
{assert(ps); // 断言:确保传入的顺序表指针ps非空,防止空指针引用导致的程序崩溃assert(ps->size > 0); // 断言:确保顺序表的大小至少为1,这个断言可能不总是必要,特别是如果允许在空表中插入assert(pos <= ps->size); // 断言:确保插入位置有效,插入位置可以在数组的最后一个元素之后(即等于size)SLCheckCapacity(ps); // 检查顺序表的容量,如果不够则进行扩容操作,保证有空间插入新元素int end = ps->size - 1; // 获取顺序表中最后一个元素的位置// 循环移动元素,为新元素腾出位置while (end >= pos){ps->a[end + 1] = ps->a[end]; // 将元素向后移动一个位置end--; // 更新处理的元素位置}ps->a[pos] = x; // 在指定位置pos放入新元素xps->size++; // 更新顺序表的元素数量
}void SLErase(SL* ps, int pos)
{assert(ps); // 断言:确保顺序表的指针ps非空,防止空指针引用导致的程序崩溃assert(pos >= 0); // 断言:确保指定的位置pos是有效的,不能是负数assert(pos < ps->size); // 断言:确保指定的位置pos在顺序表的当前元素范围内(即小于元素数量size)assert(ps->size > 0);  // 断言:检查顺序表的元素数量不为零,确保有元素可以删除int begin = pos + 1; // 设置开始移动数据的位置,从pos的下一个位置开始// 循环移动元素,覆盖需要删除的元素while (begin < ps->size){ps->a[begin - 1] = ps->a[begin]; // 将后续元素向前移动一位,填补被删除元素的位置begin++; // 移动到下一个元素}ps->size--; // 删除元素后,顺序表的元素总数减少1
}// 函数定义:在顺序表ps中查找元素x的位置
int SLFind(SL* ps, SLDateType x)
{assert(ps); // 断言:确保顺序表的指针ps非空// 遍历顺序表中的所有元素for (int i = 0; i < ps->size; i++){if (ps->a[i] == x) // 如果当前元素等于x{return i; // 返回当前元素的索引}}return -1; // 如果没有找到元素x,返回-1作为失败的标志
}// 函数定义:在顺序表ps中从索引begin开始查找元素x的位置
int SLFinds(SL* ps, SLDateType x, int begin)
{assert(ps); // 断言:确保顺序表的指针ps非空,防止空指针引用导致的程序崩溃// 从指定的开始位置begin遍历顺序表for (int i = begin; i < ps->size; i++){if (ps->a[i] == x) // 如果当前元素等于x{return i; // 返回当前元素的索引}}return -1; // 如果遍历完毕没有找到元素x,返回-1表示未找到
}void SLPrint(SL* ps)  // 打印函数
{assert(ps);for (int i = 0; i < ps->size; i++){printf("%d ", ps->a[i]);}printf("\n");
}void SLDestory(SL* ps)   //销毁
{assert(ps);if (ps->a != NULL){free(ps->a);ps->a = NULL;ps->size = ps->capacity = 0;}
}

test.c

#include "SeqList.h" // 确保包含顺序表的头文件// 主函数入口
int main()
{// 打印欢迎信息printf("*************  欢迎大家使用GalaxyPokemon的动态顺序表 **************\n");int option = 0; // 用户的选项SL s;           // 创建顺序表结构体实例SLInit(&s);     // 初始化顺序表// 使用do-while循环,直到用户选择退出程序do{menu(); // 显示菜单,函数未展示,需要自行实现printf("请输入你的操作:>");scanf("%d", &option); // 接收用户选择的操作// 根据用户输入执行相应的操作switch (option){case 1: // 尾插操作printf("请依次输入你要尾插的数据:,以-1结束\n");int sum = 0;scanf("%d", &sum);while (sum != -1){SLPushBack(&s, sum); // 尾插数据scanf("%d", &sum);}break;case 2: // 尾删操作SLPopBack(&s); // 尾删数据break;case 3: // 头插操作int x = 0;scanf("%d", &x);SLPushFront(&s, x); // 头插数据break;case 4: // 头删操作SLPopFront(&s); // 头删数据break;case 5: // 任意位置插入操作SLInsert(&s, 5, 20); // 在位置5插入数据20break;case 6: // 任意位置删除操作SLErase(&s, 3); // 删除位置3的数据break;case 7: // 查找并删除特定数据int z = 0;printf("请输入要删除序列的中的某个数字\n");scanf("%d", &z);int y = SLFind(&s, z); // 查找数据z的位置printf("%d的位置在%d处: \n", z, y);if (y != -1) // 如果找到,则删除{SLErase(&s, y);}break;case 8: // 删除顺序表中所有的某个数据int w = 0;printf("请输入要删除序列的中的数字\n");scanf("%d", &w);int pos = SLFinds(&s, w, 0); // 查找数据w的位置while (pos != -1) // 循环删除所有w{SLErase(&s, pos);pos = SLFinds(&s, w, pos);}break;case 9: // 打印顺序表SLPrint(&s); // 打印顺序表中的数据break;default: // 处理无效输入if (option == -1){SLDestory(&s); // 销毁顺序表,释放资源exit(0);       // 退出程序 }else{printf("输入错误,请重新输入\n");}break;}} while (option != -1); // 当用户输入-1时退出循环SLDestory(&s); // 循环结束后销毁顺序表,释放资源return 0; // 程序正常退出
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/4464.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Github进行fork后如何与原仓库同步[解决git clone 太慢的问题]

前言 fork了一个仓库以后怎么同步源仓库的代码&#xff1f; 先说一下git clone太慢的问题&#xff0c;可以通过代理拉取代码&#xff0c;具体请看&#xff1a; https://gitclone.com/ 步骤 1、执行命令 git remote -v 查看你的远程仓库的路径。 以一个实际例子说明&#x…

docker容器通俗理解

前言 如果大家没使用过Docker,就在电脑上下载一个VMware Workstation Pro&#xff0c;创建一个虚拟机安装一个windows操作一下感受一下,为什么我的电脑上还以再安装一台windows主机&#xff1f;其实你可以理解为Docker就是Linux系统的一个虚拟机软件。 我的Windows也可以安装…

【高校科研前沿】东北地理所孙敬轩博士为一作在《中国科学:地球科学(中英文版)》发文:气候变化下东北地区农业绿水安全风险评估

目录 01 文章简介 02 研究内容 03 文章引用 04 期刊简介 01 文章简介 论文名称&#xff1a;Risk assessment of agricultural green water security in Northeast China under climate change&#xff08;气候变化下东北地区农业绿水安全风险评估&#xff09; 第一作者及…

猫咪冻干究竟怎么选不踩雷?这几款生骨肉冻干一定适合你家喵

315中国之声的报道揭示了河北省邢台市南和区某宠粮代工厂的“行业秘密”&#xff0c;这无疑给广大宠物主人敲响了警钟。配料表上标注的鸡肉含量高达52%&#xff0c;新鲜鸡小胸占20%&#xff0c;然而所谓的鲜鸡肉实际上却是鸡肉粉。我们养宠物本是为了心灵上的慰藉&#xff0c;但…

栈和队列总结

文章目录 前言一、栈和队列的实现1.栈的具体实现2.循环顺序队列的具体实现 二、栈和队列总结总结 前言 T_T此专栏用于记录数据结构及算法的&#xff08;痛苦&#xff09;学习历程&#xff0c;便于日后复习&#xff08;这种事情不要啊&#xff09;。所用教材为《数据结构 C语言版…

揭秘Linux文件系统

前言 在上一篇文件描述符详解中谈论的都是打开的文件&#xff0c;但是在我们的系统中不仅有打开的文件还有许多未打开的文件&#xff0c;那么这些未打开的文件又该如何理解呢?阅读完本篇文章相信你会得到答案。 如果觉得文章内容对你有所帮助的话&#xff0c;可以给博主一键三…

新装电脑Flutter环境部署坑汇总(持续更新)

1.本地安装&#xff0c;安装fvm的坑 本人电脑使用windows &#xff0c;安装fvm则一般使用choco安装&#xff0c;那么首先需要安装choco,打开powershell/或者cmd运行以下命令&#xff1a; Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager…

图像置乱加密-Arnold加密算法

置乱加密是另一种较常用的加密方法&#xff0c;现也被许多文献选用&#xff0c;置乱加密可以是以像素为单位进行全局置乱&#xff0c;该方式打乱了图像像素值的位置&#xff0c;使其图像内容失去相关性&#xff0c;达到保护的目的。也可以是以块为单位进行置乱&#xff0c;该方…

Kafka报错ERROR Exiting Kafka due to fatal exception during startup

报错&#xff1a; ERROR Exiting Kafka due to fatal exception during startup. (kafka.Kafka$) kafka.common.InconsistentClusterIdException: The Cluster ID FSzSO50oTLCRhRnRylihcg doesnt match stored clusterId Some(0oSLohwtQZWbIi73YUMs8g) in meta.properties. Th…

某宝因SSL证书过期无法正常访问,证书过期问题频发企业如何破局?

近日&#xff0c;有网友发现&#xff0c;某宝网站无法正常访问&#xff0c;出现“此连接非私人连接”提示&#xff0c;而导致此类提示的罪魁祸首是因为SSL证书过期&#xff01;其实&#xff0c;近年来&#xff0c;因SSL证书过期导致的网站无法正常访问的事件频频发生&#xff0…

vue2—— mixin 超级详细!!!

mixin Mixin是面向对象程序设计语言中的类&#xff0c;提供了方法的实现。其他类可以访问mixin类的方法而不必成为其子类 Mixin类通常作为功能模块使用&#xff0c;在需要该功能时“混入”&#xff0c;有利于代码复用又避免了多继承的复杂 vue中的mixin 先来看一下官方定义 mi…

YOLO-yolov5构建数据集

1.收集数据集 创建一个dataset文件夹用来存放图片数据集。 我这里使用的图片数据集&#xff0c;是对一段视频进行抽帧得到的200张狗狗图片。 在dataset文件夹下新建images和labels文件夹&#xff0c;并将200张狗狗图片放入images中。 2.标注数据集 2.1安装标注工具labelimg…

Jenkins邮件发送失败问题解决

如下提示为 Extended E-mail Notification开启Debug模式下显示的错误信息&#xff0c; (Debug模式设置方法&#xff1a;Dashboard-> manage Jenkins->configure System)DEBUG SMTP: Attempt to authenticate using mechanisms: LOGIN PLAIN DIGEST-MD5 NTLM XOAUTH2 DEB…

OpenCV——Bernsen局部阈值二值化方法

目录 一、Bernsen算法1、算法概述2、参考文献二、代码实现三、结果展示Bernsen局部阈值二值化方法由CSDN点云侠原创,爬虫自重。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、Bernsen算法 1、算法概述 Bernsen 算法是另一种流行的局部阈值二值化方…

网络相关知识总结

1、网口设置 网口设置IP&#xff0c;即操作/etc/sysconfig/network-scripts路径下的ifcfg-xx文件 主要参数详解&#xff1a; DEVICE:网口名 ONBOOT&#xff1a;表示启动系统时是否激活网卡&#xff0c;yes为激活&#xff0c;no不激活 HWADDR:mac值 DEFROUTE://默认路由设置…

Docker容器:docker基础

目录 一.docker前言 云计算服务模式 关于 docker 产品 虚拟化产品有哪些&#xff1f; ① 寄居架构 ② 源生架构 虚拟化技术概述 主流虚拟化产品对比 1. VMware系列 2. KVM/OpenStack 3. Xen 4. 其他半/全虚拟化产品 二. docker 的相关知识 1. docker 的概念 doc…

[linux网络编程]UDP协议和TCP协议的使用

目录 看以下内容前&#xff0c;你要先了解main函数带参数有什么用、 了解socket的相关函数接口 如果不了解socket的相关函数接口请先看我这篇文章 main函数带参数有什么用 UDP udp_server 1.生成socket文件描述符 2.填充sockaddr_in信息 3.bind 4.发&#xff08;收&…

学习100个Unity Shader (14) ---透明效果

文章目录 渲染队列透明度测试&#xff08;Alpha Test&#xff09;效果Shader 透明度混合&#xff08;Alpha Blending&#xff09;效果Shader 参考 渲染队列 由”Queue“ 标签决定&#xff0c;索引号越小越早被渲染&#xff1a; 名称队列索引号Background1000Geometry2000Alph…

从虚拟化走向云原生,红帽OpenShift“一手托两家”

汽车行业已经迈入“软件定义汽车”的新时代。吉利汽车很清醒地意识到&#xff0c;只有通过云原生技术和数字化转型&#xff0c;才能巩固其作为中国领先汽车制造商的地位。 和很多传统企业一样&#xff0c;吉利汽车在走向云原生的过程中也经历了稳态业务与敏态业务并存带来的前所…

牛客NC98 判断t1树中是否有与t2树完全相同的子树【simple 深度优先dfs C++/Java/Go/PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/4eaccec5ee8f4fe8a4309463b807a542 思路 深度优先搜索暴力匹配 思路和算法这是一种最朴素的方法——深度优先搜索枚举 s 中的每一个节点&#xff0c;判断这个点的子树是否和 t 相等。如何判断一个节点的子树是否…