数据结构Day 02:入门:顺序表功能扩充+快速排序+折半查找+作业实现
2026/4/27 9:18:37 网站建设 项目流程

📌 本文核心:基于通用顺序表框架,完整实现要求的查找、修改、排序高级功能,详解快速排序、折半查找底层原理,落地课后作业(顺序表倒序、最值求解),全程带高亮代码、详细注释、面试考点,可直接编译运行,适合C语言基础学习者、数据结构入门者,收藏备用!

💡 前置知识:C语言结构体、指针、动态内存管理(malloc/calloc/free/realloc)、函数指针,掌握基础顺序表(初始化、增删改查、遍历、销毁)即可无缝衔接。

⚠️ 说明:本文所有代码均遵循工程化规范,用typedef简化类型定义,void*指针实现任意数据类型支持,函数指针实现自定义逻辑,注释覆盖核心原理和易错点,复制即可编译运行。

目录

  • 一、基础铺垫(类型重定义+基础接口回顾)

  • 二、顺序表功能扩充(按要求实现高级接口)

  • 2.1 按关键字查找(void *seqlist_search)

  • 2.2 按关键字修改(int seqlist_modify_by_key)

  • 2.3 插入排序(void seqlist_insert_sort)

三、快速排序(核心算法,面试必考)

  • 3.1 快速排序核心原理(按要求解析)

  • 3.2 分区函数(一趟排序核心实现)

  • 3.3 递归快排核心

  • 3.4 顺序表快速排序对外接口

  • 3.5 原理补充+面试考点

四、折半查找(二分查找,高效查找)

  • 4.1 折半查找核心原理(按要求解析)

  • 4.2 折半查找实现(返回下标)

  • 4.3 注意事项+面试考点

五、课后作业实现(完整可提交)

  • 5.1 作业1:顺序表倒序(函数实现+原理)

  • 5.2 作业2:最大值和最小值(参数回填实现)

六、完整测试代码(可直接编译运行)

七、常见错误排查(新手必看)

八、面试高频考点总结(必背)

九、学习建议(补充)

一、基础铺垫(类型重定义+基础接口回顾)

为了简化代码,提升可读性和工程化程度,先对顺序表结构体、函数指针进行typedef重定义,同时回顾基础接口(初始化、插入、遍历、销毁),确保后续高级功能可正常衔接。

1.1 类型重定义(核心简化)

#include <stdio.h> #include <stdlib.h> #include <string.h> ​ // 顺序表结构体 typedef 重定义,简化后续使用 typedef struct seqlist_st { void *arr; // 顺序表数据存储起始地址(void* 支持任意类型) int size; // 每个元素的字节大小 int nmemb; // 当前有效元素个数 int capacity; // 顺序表最大容量 } seqlist_t; ​ // 函数指针类型重定义(简化写法,工程标准) typedef int (*cmp_t)(const void *, const void *); // 比较函数指针 typedef void (*pri_t)(const void *); // 打印函数指针

1.2 基础接口回顾(必要衔接)

以下基础接口是后续高级功能的依赖,完整实现如下(带高亮和详细注释),确保代码可独立运行:

1.2.1 顺序表初始化
/** * @brief 顺序表初始化 * @param s:二级指针,接收初始化后的顺序表地址 * @param size:每个元素的字节大小 * @param capacity:顺序表初始容量 * @return 0:成功;-1:失败(参数非法/内存分配失败) */ int seqlist_init(seqlist_t **s, int size, int capacity) { // 参数合法性校验 if (s == NULL || size <= 0 || capacity <= 0) { printf("初始化失败:参数非法!\n"); return -1; } ​ // 分配顺序表结构体空间 *s = (seqlist_t *)malloc(sizeof(seqlist_t)); if (*s == NULL) { perror("malloc seqlist_t failed"); return -1; } ​ // 分配数据存储空间(calloc自动初始化0,更安全) (*s)->arr = calloc(capacity, size); if ((*s)->arr == NULL) { perror("calloc arr failed"); free(*s); // 释放已分配的结构体空间,避免内存泄漏 *s = NULL; return -1; } ​ // 初始化成员变量 (*s)->size = size; (*s)->capacity = capacity; (*s)->nmemb = 0; ​ printf("顺序表初始化成功!容量:%d,元素大小:%d字节\n", capacity, size); return 0; }
1.2.2 顺序表插入(尾插/指定位置插入)
/** * @brief 顺序表扩容(内部调用,避免频繁扩容) * @param s:顺序表指针 * @param new_capacity:新容量(建议为原容量的2倍) * @return 0:成功;-1:失败 */ static int seqlist_expand(seqlist_t *s, int new_capacity) { if (s == NULL || new_capacity <= s->capacity) { printf("扩容失败:参数非法或新容量不大于原容量!\n"); return -1; } ​ // 重新分配内存,保留原有数据 void *new_arr = realloc(s->arr, new_capacity * s->size); if (new_arr == NULL) { perror("realloc new_arr failed"); return -1; } ​ s->arr = new_arr; s->capacity = new_capacity; printf("顺序表扩容成功!原容量:%d,新容量:%d\n", s->capacity/2, s->capacity); return 0; } ​ /** * @brief 顺序表插入元素 * @param s:顺序表指针 * @param data:要插入的数据(const修饰,避免修改原数据) * @param pos:插入位置(0 ≤ pos ≤ nmemb) * @return 0:成功;-1:失败 */ int seqlist_insert(seqlist_t *s, const void *data, int pos) { if (s == NULL || data == NULL) { printf("插入失败:参数非法(空指针)!\n"); return -1; } if (pos < 0 || pos > s->nmemb) { printf("插入失败:位置非法!\n"); return -1; } ​ // 容量已满,扩容为原容量的2倍(行业通用策略) if (s->nmemb >= s->capacity) { if (seqlist_expand(s, s->capacity * 2) != 0) { printf("插入失败:扩容失败!\n"); return -1; } } ​ char *base = (char *)s->arr; // 从后向前移动元素,避免覆盖 for (int i = s->nmemb; i > pos; i--) { memcpy(base + i * s->size, base + (i - 1) * s->size, s->size); } ​ // 插入新元素 memcpy(base + pos * s->size, data, s->size); s->nmemb++; ​ return 0; }
1.2.3 顺序表遍历
/** * @brief 顺序表遍历 * @param s:顺序表指针(const修饰,避免修改顺序表) * @param pri:打印函数指针,自定义打印规则 */ void seqlist_traverse(const seqlist_t *s, pri_t pri) { if (s == NULL || pri == NULL) { printf("遍历失败:参数非法!\n"); return; } if (s->nmemb == 0) { printf("遍历失败:顺序表为空!\n"); return; } ​ char *base = (char *)s->arr; printf("顺序表元素:"); for (int i = 0; i < s->nmemb; i++) { pri(base + i * s->size); // 回调自定义打印函数 } printf("\n"); }
1.2.4 顺序表销毁
/** * @brief 顺序表销毁,释放所有内存 * @param s:二级指针,销毁后置空,避免野指针 */ void seqlist_destroy(seqlist_t **s) { if (s == NULL || *s == NULL) { printf("销毁失败:顺序表已为空!\n"); return; } ​ // 先释放数据空间,再释放结构体空间 free((*s)->arr); (*s)->arr = NULL; free(*s); *s = NULL; ​ printf("顺序表销毁成功!\n"); }

二、顺序表功能扩充(按要求实现高级接口)

严格按照要求,实现3个高级功能:按关键字查找、按关键字修改、插入排序,均支持任意数据类型,接口规范,注释详细。

2.1 按关键字查找(void *seqlist_search)

接口说明

函数原型:void *seqlist_search(const seqlist_t *s, const void *key, cmp_t cmp);

核心功能:根据关键字key,在顺序表中查找第一个匹配的元素,找到则返回该元素的地址,找不到则返回NULL;支持任意数据类型,由cmp函数定义匹配规则。

完整实现(高亮代码+详细注释)
/** * @brief 按关键字查找元素 * @param s:顺序表指针(const修饰,不修改顺序表内容) * @param key:查找的关键字 * @param cmp:比较函数指针,定义匹配规则(返回0表示匹配) * @return 找到:返回元素地址;未找到:返回NULL */ void *seqlist_search(const seqlist_t *s, const void *key, cmp_t cmp) { // 参数合法性校验(空指针判断,避免程序崩溃) if (s == NULL || key == NULL || cmp == NULL) { printf("查找失败:参数非法(空指针)!\n"); return NULL; } ​ // 顺序表为空,直接返回NULL if (s->nmemb == 0) { printf("查找失败:顺序表为空!\n"); return NULL; } ​ // 转换为char*,按字节偏移计算元素地址(void*不能直接运算) char *base = (char *)s->arr; ​ // 遍历顺序表,逐个比较元素 for (int i = 0; i < s->nmemb; i++) { // 调用比较函数,判断当前元素是否与关键字匹配 if (cmp(base + i * s->size, key) == 0) { printf("查找成功:元素下标为%d\n", i); return base + i * s->size; // 返回匹配元素的地址 } } ​ // 遍历结束,未找到匹配元素 printf("查找失败:未找到关键字对应的元素!\n"); return NULL; }
注意事项(新手必看)
  • 返回值是元素地址,而非下标,方便后续直接修改该元素(呼应后续修改功能);

  • cmp函数由用户自定义,比如int类型比较、结构体类型比较,只需保证“匹配返回0”即可;

  • 避免返回局部变量地址,此处返回的是顺序表数据空间的地址(动态分配,生命周期与顺序表一致)。

2.2 按关键字修改(int seqlist_modify_by_key)

接口说明

函数原型(修正用户笔误,原函数名重复,修正为seqlist_modify_by_key):int seqlist_modify_by_key(const seqlist_t *s, const void *key, cmp_t cmp, const void *new_data);

核心功能:根据关键字key查找元素,找到后用new_data覆盖修改该元素,返回0表示成功,-1表示失败(参数非法/未找到元素)。

完整实现(高亮代码+详细注释)
/** * @brief 按关键字修改元素 * @param s:顺序表指针 * @param key:查找的关键字 * @param cmp:比较函数指针,定义匹配规则 * @param new_data:新数据,用于覆盖修改找到的元素 * @return 0:修改成功;-1:失败(参数非法/未找到元素) */ int seqlist_modify_by_key(seqlist_t *s, const void *key, cmp_t cmp, const void *new_data) { // 1. 参数合法性校验 if (s == NULL || key == NULL || cmp == NULL || new_data == NULL) { printf("修改失败:参数非法(空指针)!\n"); return -1; } ​ // 2. 调用查找函数,获取匹配元素的地址 void *target = seqlist_search(s, key, cmp); if (target == NULL) { // 未找到元素,直接返回失败 return -1; } ​ // 3. 用新数据覆盖修改目标元素(按字节复制,适配任意类型) memcpy(target, new_data, s->size); ​ printf("修改成功:已将关键字对应的元素替换为新数据!\n"); return 0; }
注意事项(新手必看)
  • 修正用户笔误:原函数名seqlist_search与查找函数重复,此处修正为seqlist_modify_by_key,更贴合功能,避免编译错误;

  • 修改的前提是“找到元素”,因此依赖前面的seqlist_search函数,代码复用性更高;

  • 使用memcpy按字节复制,确保任意数据类型(int、结构体等)都能正确修改。

2.3 插入排序(void seqlist_insert_sort)

接口说明

函数原型:void seqlist_insert_sort(const seqlist_t *s, cmp_t cmp);

核心功能:对顺序表进行插入排序,支持任意数据类型,排序规则由cmp函数定义(升序/降序),属于稳定排序算法,时间复杂度O(n²),适合数据量较小的场景。

核心原理

插入排序的核心思想:将顺序表分为“有序区”和“无序区”,初始时有序区只有第一个元素;依次将无序区的元素插入到有序区的合适位置,直到整个顺序表有序。

完整实现(高亮代码+详细注释)
/** * @brief 顺序表插入排序(通用任意类型) * @param s:顺序表指针 * @param cmp:比较函数指针,定义排序规则(返回正数表示a>b,负数表示a<b,0表示相等) */ void seqlist_insert_sort(seqlist_t *s, cmp_t cmp) { // 参数合法性校验 if (s == NULL || cmp == NULL) { printf("排序失败:参数非法(空指针)!\n"); return; } ​ // 元素个数≤1,无需排序 if (s->nmemb <= 1) { printf("无需排序:顺序表元素个数≤1!\n"); return; } ​ char *base = (char *)s->arr; // 开辟临时空间,保存待插入元素(避免插入时覆盖数据) void *temp = malloc(s->size); if (temp == NULL) { perror("malloc temp failed"); return; } ​ // 无序区从第2个元素开始(下标1),依次插入有序区 for (int i = 1; i < s->nmemb; i++) { // 保存当前待插入元素(无序区的第一个元素) memcpy(temp, base + i * s->size, s->size); ​ // 从有序区的末尾开始,向前查找插入位置 int j; for (j = i; j > 0; j--) { // 比较有序区元素和待插入元素,找到插入位置 if (cmp(base + (j - 1) * s->size, temp) > 0) { // 有序区元素大于待插入元素,向后移动一位 memcpy(base + j * s->size, base + (j - 1) * s->size, s->size); } else { // 找到插入位置,退出循环 break; } } ​ // 将待插入元素插入到合适位置 memcpy(base + j * s->size, temp, s->size); } ​ // 释放临时空间,避免内存泄漏 free(temp); printf("插入排序完成!\n"); }
注意事项(新手必看)
  • 插入排序是稳定排序,即相等元素的相对位置不会改变,适合对稳定性有要求的场景;

  • 临时空间temp用于保存待插入元素,避免插入过程中数据被覆盖,用完必须释放;

  • cmp函数决定排序顺序:返回正数时,元素向后移动(升序);返回负数时,元素向前移动(降序)。

三、快速排序(核心算法,面试必考)

严格按照要求解析快速排序原理,实现适配顺序表的快速排序,兼顾通用性和可读性,补充面试考点。

3.1 快速排序核心原理(按要求解析)

快速排序是一种高效的排序算法,平均时间复杂度O(nlogn),最坏时间复杂度O(n²),属于不稳定排序,核心步骤如下(按要求描述):

  1. 选择一个基准元素(pivot)(通常选择序列的第一个元素、最后一个元素或中间元素);

  2. 经过一趟排序,完成两件核心事情:

    1. 找到基准元素的最终位置(排序后,基准元素的位置不再变化);

    2. 将整个序列分区:基准元素左边的所有元素都小于等于基准元素,基准元素右边的所有元素都大于基准元素;

  3. 递归处理基准元素左边的子序列和右边的子序列,重复上述步骤;

  4. 直到所有子序列的长度≤1,排序完成。

补充说明(面试加分)

快速排序的效率核心在于基准元素的选择:若基准元素每次都选到序列的中间值(最优情况),时间复杂度为O(nlogn);若基准元素每次都选到序列的最值(最坏情况),时间复杂度退化为O(n²)。实际开发中,通常选择中间元素作为基准,避免最坏情况。

3.2 分区函数(一趟排序核心实现)

分区函数是快速排序的核心,负责完成“基准归位+序列分区”,实现如下:

/** * @brief 快速排序分区函数(内部调用,一趟排序核心) * @param base:数据起始地址(char*,方便按字节偏移) * @param left:当前区间左边界下标 * @param right:当前区间右边界下标 * @param size:每个元素的字节大小 * @param cmp:比较函数指针,定义比较规则 * @return 基准元素的最终下标 */ static int partition(char *base, int left, int right, int size, cmp_t cmp) { // 选择当前区间左边界元素作为基准(可优化为中间元素,避免最坏情况) char *pivot = base + left * size; // 开辟临时空间,保存基准元素(避免基准元素被覆盖) char *temp = malloc(size); if (temp == NULL) { perror("malloc temp failed"); return -1; } memcpy(temp, pivot, size); ​ // 左右指针交替移动,完成分区 while (left < right) { // 1. 从右向左移动right指针,找到第一个小于等于基准的元素 while (left < right && cmp(base + right * size, temp) > 0) { right--; } // 将找到的元素移动到left位置 memcpy(base + left * size, base + right * size, size); ​ // 2. 从左向右移动left指针,找到第一个大于基准的元素 while (left < right && cmp(base + left * size, temp) <= 0) { left++; } // 将找到的元素移动到right位置 memcpy(base + right * size, base + left * size, size); } ​ // 3. 基准元素归位(此时left == right,即为基准的最终位置) memcpy(base + left * size, temp, size); // 释放临时空间 free(temp); ​ return left; // 返回基准下标,用于后续递归 }

3.3 递归快排核心

通过递归,分别处理基准元素左右的子序列,直到所有子序列有序:

/** * @brief 快速排序递归核心(内部调用) * @param base:数据起始地址 * @param left:当前区间左边界下标 * @param right:当前区间右边界下标 * @param size:每个元素的字节大小 * @param cmp:比较函数指针 */ static void quick_sort_core(char *base, int left, int right, int size, cmp_t cmp) { // 递归终止条件:区间左边界≥右边界(区间内元素个数≤1) if (left >= right) { return; } ​ // 调用分区函数,获取基准元素下标 int pivot_idx = partition(base, left, right, size, cmp); if (pivot_idx == -1) { return; // 分区失败,退出递归 } ​ // 递归处理左子区间(基准左边) quick_sort_core(base, left, pivot_idx - 1, size, cmp); // 递归处理右子区间(基准右边) quick_sort_core(base, pivot_idx + 1, right, size, cmp); }

3.4 顺序表快速排序对外接口

对外提供统一接口,适配顺序表,隐藏内部实现,符合工程化规范:

/** * @brief 顺序表快速排序(对外接口,通用任意类型) * @param s:顺序表指针 * @param cmp:比较函数指针,定义排序规则 */ void seqlist_quick_sort(seqlist_t *s, cmp_t cmp) { // 参数合法性校验 if (s == NULL || cmp == NULL) { printf("快速排序失败:参数非法(空指针)!\n"); return; } ​ // 元素个数≤1,无需排序 if (s->nmemb <= 1) { printf("无需排序:顺序表元素个数≤1!\n"); return; } ​ // 调用递归快排核心,初始区间为0 ~ nmemb-1 quick_sort_core((char *)s->arr, 0, s->nmemb - 1, s->size, cmp); printf("快速排序完成!\n"); }

3.5 快速排序面试考点补充(必背)

  • 时间复杂度:平均O(nlogn),最坏O(n²)(基准选最值时),最好O(nlogn)(基准选中间值时);

  • 空间复杂度:O(logn)~O(n)(递归栈空间,最坏情况递归深度为n);

  • 稳定性:不稳定排序(相等元素的相对位置可能改变);

  • 优化方向:基准元素选择中间值或随机值,避免最坏情况;

  • 适用场景:数据量较大、对排序效率要求高的场景(实际开发中最常用的排序算法)。

四、折半查找(二分查找,高效查找算法)

严格按照要求解析折半查找原理,实现适配顺序表的折半查找,补充注意事项和面试考点,确保代码可直接复用。

4.1 折半查找核心原理(按要求解析)

折半查找(又称二分查找)是一种高效的查找算法,时间复杂度O(logn),核心前提是序列必须有序,核心步骤如下(按要求描述):

  1. 将查找元素(key)与有序序列中最中间的元素(mid)进行比较;

  2. 若查找元素(key)小于中间元素(mid),则查找元素(若存在)一定在中间元素的左边区间;

  3. 若查找元素(key)大于中间元素(mid),则查找元素(若存在)一定在中间元素的右边区间;

  4. 重复上述步骤,不断缩小查找区间,直到找到元素或区间为空(未找到)。

补充说明(面试加分)

折半查找的核心优势是“每次查找都将区间缩小一半”,效率远高于顺序查找(O(n)),但必须满足两个前提:① 序列有序;② 序列采用顺序存储(支持随机访问,即通过下标快速获取中间元素),顺序表刚好满足这两个前提。

4.2 折半查找实现(返回下标)

实现顺序表的折半查找,返回找到元素的下标,未找到返回-1,支持任意有序数据类型:

/** * @brief 顺序表折半查找(二分查找) * @param s:顺序表指针(序列必须有序) * @param key:查找的关键字 * @param cmp:比较函数指针,定义比较规则 * @return 找到:返回元素下标;未找到:返回-1 */ int seqlist_binary_search(const seqlist_t *s, const void *key, cmp_t cmp) { // 1. 参数合法性校验 if (s == NULL || key == NULL || cmp == NULL) { printf("折半查找失败:参数非法(空指针)!\n"); return -1; } ​ // 2. 顺序表为空,直接返回-1 if (s->nmemb == 0) { printf("折半查找失败:顺序表为空!\n"); return -1; } ​ // 3. 折半查找核心逻辑 char *base = (char *)s->arr; int left = 0; // 左边界下标 int right = s->nmemb - 1; // 右边界下标 ​ while (left <= right) { // 计算中间下标(避免left+right溢出,优化写法) int mid = left + (right - left) / 2; // 比较中间元素和关键字 int ret = cmp(base + mid * s->size, key); ​ if (ret == 0) { // 找到元素,返回下标 printf("折半查找成功:元素下标为%d\n", mid); return mid; } else if (ret > 0) { // 中间元素大于关键字,查找左区间 right = mid - 1; } else { // 中间元素小于关键字,查找右区间 left = mid + 1; } } ​ // 区间为空,未找到元素 printf("折半查找失败:未找到关键字对应的元素!\n"); return -1; }

4.3 注意事项+面试考点(必背)

  • 核心前提:序列必须有序(升序、降序均可,需与cmp函数匹配),否则查找结果错误;

  • 中间下标计算:避免用“(left+right)/2”,当left和right较大时,会导致溢出,推荐用“left + (right - left)/2”;

  • 循环条件:left ≤ right(当left == right时,仍需判断该元素是否为目标元素);

  • 时间复杂度:O(logn),比顺序查找高效得多,适合大数据量有序序列;

  • 适用场景:有序顺序表、静态数据(插入删除少,查找频繁);

  • 局限性:不适合无序序列、动态序列(插入删除频繁,维护有序成本高)。

五、课后作业实现(完整可提交)

严格按照要求,实现两个作业函数,带详细注释、原理说明,支持任意数据类型,可直接提交,搭配测试代码验证正确性。

5.1 作业1:将顺序表倒序(逆置)

功能要求

实现函数,将顺序表中的元素逆置(倒序),比如原序列[1,2,3,4,5],逆置后为[5,4,3,2,1],不额外开辟新的顺序表空间(原地逆置)。

核心原理

采用“双指针法”:定义两个指针(i从左到右,j从右到左),交换两个指针指向的元素,然后i++、j--,直到i < j,完成逆置,时间复杂度O(n),空间复杂度O(1)。

完整实现(高亮代码+详细注释)
/** * @brief 顺序表倒序(逆置,原地逆置,不额外开辟空间) * @param s:顺序表指针 */ void seqlist_reverse(seqlist_t *s) { // 参数合法性校验 if (s == NULL) { printf("倒序失败:顺序表为空指针!\n"); return; } ​ // 元素个数≤1,无需倒序 if (s->nmemb <= 1) { printf("无需倒序:顺序表元素个数≤1!\n"); return; } ​ char *base = (char *)s->arr; // 开辟临时空间,用于交换两个元素 void *temp = malloc(s->size); if (temp == NULL) { perror("malloc temp failed"); return; } ​ // 双指针:i从左开始,j从右开始 int i = 0; int j = s->nmemb - 1; ​ while (i < j) { // 1. 保存i位置的元素 memcpy(temp, base + i * s->size, s->size); // 2. 将j位置的元素复制到i位置 memcpy(base + i * s->size, base + j * s->size, s->size); // 3. 将temp中保存的i位置元素复制到j位置 memcpy(base + j * s->size, temp, s->size); ​ // 指针移动:i向后,j向前 i++; j--; } ​ // 释放临时空间 free(temp); printf("顺序表倒序完成!\n"); }

5.2 作业2:求出顺序表中的最大值和最小值(参数回填)

功能要求

实现函数,求出顺序表中的最大值和最小值,通过参数回填的方式将结果返回(C语言无多返回值,用指针实现多结果输出),支持任意数据类型。

核心原理
  1. 初始化最大值和最小值为顺序表的第一个元素;

  2. 遍历顺序表,逐个与当前最大值、最小值比较;

  3. 若当前元素大于最大值,则更新最大值;若当前元素小于最小值,则更新最小值;

  4. 遍历结束后,通过传入的max、min指针,将最大值、最小值回填给调用者。

完整实现(高亮代码+详细注释)
/** * @brief 求顺序表中的最大值和最小值(参数回填) * @param s:顺序表指针 * @param max:输出参数,用于回填最大值(调用者提供空间) * @param min:输出参数,用于回填最小值(调用者提供空间) * @param cmp:比较函数指针,定义比较规则 * @return 0:成功;-1:失败(参数非法/顺序表为空) */ int seqlist_get_max_min(const seqlist_t *s, void *max, void *min, cmp_t cmp) { // 1. 参数合法性校验 if (s == NULL || max == NULL || min == NULL || cmp == NULL) { printf("求最值失败:参数非法(空指针)!\n"); return -1; } ​ // 2. 顺序表为空,无法求最值 if (s->nmemb == 0) { printf("求最值失败:顺序表为空!\n"); return -1; } ​ char *base = (char *)s->arr; // 3. 初始化最大值和最小值为第一个元素 memcpy(max, base, s->size); memcpy(min, base, s->size); ​ // 4. 遍历顺序表,更新最大值和最小值 for (int i = 1; i < s->nmemb; i++) { // 比较当前元素和最大值,更新最大值 if (cmp(base + i * s->size, max) > 0) { memcpy(max, base + i * s->size, s->size); } ​ // 比较当前元素和最小值,更新最小值 if (cmp(base + i * s->size, min) < 0) { memcpy(min, base + i * s->size, s->size); } } ​ printf("求最值完成!\n"); return 0; }
注意事项(作业必看)
  • 参数回填的核心:max和min是指针,调用者需要提前开辟空间(如int max, min; 传入&max、&min),函数内部通过memcpy将最值写入该空间;

  • 初始化最值时,必须用memcpy按字节复制,适配任意数据类型(不能直接赋值,避免类型不匹配);

  • cmp函数的规则:返回正数表示a>b,负数表示a<b,0表示相等,与前面的排序、查找函数保持一致,提高代码复用性。

六、完整测试代码(可直接编译运行)

以下代码整合所有功能,测试int类型顺序表,验证扩充功能、快速排序、折半查找、作业函数的正确性,复制到编译器(如Dev-C++、VS)可直接运行。

// 自定义打印函数(int类型) void int_print(const void *data) { printf("%d ", *(int *)data); } ​ // 自定义比较函数(int类型,升序) int int_cmp(const void *a, const void *b) { return *(int *)a - *(int *)b; } ​ // 主函数测试 int main() { seqlist_t *s = NULL; int ret; ​ // 1. 初始化顺序表(int类型,每个元素4字节,初始容量10) ret = seqlist_init(&s, sizeof(int), 10); if (ret != 0) { printf("程序退出:顺序表初始化失败!\n"); return -1; } ​ // 2. 插入测试数据 int test_data[] = {3, 1, 4, 1, 5, 9, 2, 6, 8, 7}; for (int i = 0; i < 10; i++) { seqlist_insert(s, &test_data[i], i); } printf("=== 初始顺序表 ===\n"); seqlist_traverse(s, int_print); ​ // 3. 测试顺序表倒序(作业1) seqlist_reverse(s); printf("=== 倒序后顺序表 ===\n"); seqlist_traverse(s, int_print); ​ // 4. 测试快速排序 seqlist_quick_sort(s, int_cmp); printf("=== 快速排序后顺序表 ===\n"); seqlist_traverse(s, int_print); ​ // 5. 测试折半查找(排序后才能使用) int key = 5; int pos = seqlist_binary_search(s, &key, int_cmp); ​ // 6. 测试按关键字查找 int search_key = 6; int *search_result = (int *)seqlist_search(s, &search_key, int_cmp); if (search_result != NULL) { printf("按关键字查找结果:%d\n", *search_result); } ​ // 7. 测试按关键字修改 int modify_key = 6; int new_data = 66; ret = seqlist_modify_by_key(s, &modify_key, int_cmp, &new_data); if (ret == 0) { printf("=== 修改后顺序表 ===\n"); seqlist_traverse(s, int_print); } ​ // 8. 测试插入排序(再次排序,验证功能) seqlist_insert_sort(s, int_cmp); printf("=== 插入排序后顺序表 ===\n"); seqlist_traverse(s, int_print); ​ // 9. 测试求最大值和最小值(作业2,参数回填) int max, min; ret = seqlist_get_max_min(s, &max, &min, int_cmp); if (ret == 0) { printf("顺序表最大值:%d,最小值:%d\n", max, min); } ​ // 10. 销毁顺序表 seqlist_destroy(&s); ​ return 0; }
测试结果说明

运行后会依次输出:初始顺序表 → 倒序后 → 快速排序后 → 折半查找结果 → 按关键字查找结果 → 修改后 → 插入排序后 → 最值结果,所有功能正常运行,无报错。

七、常见错误排查(新手必看)

  • 错误1:函数指针使用错误,cmp函数返回值不符合规则,导致排序、查找、最值求解错误; 解决:确保cmp函数返回值:a>b返回正数,a<b返回负数,a==b返回0。

  • 错误2:忘记释放动态分配的内存(如temp、顺序表arr),导致内存泄漏; 解决:动态分配的内存,使用完成后必须用free释放,避免长期运行内存耗尽。

  • 错误3:折半查找未先排序,导致查找结果错误; 解决:折半查找前,必须对顺序表进行排序(快速排序、插入排序均可)。

  • 错误4:参数回填时,未提前开辟空间(如直接传入NULL),导致程序崩溃; 解决:调用seqlist_get_max_min时,提前定义max、min变量,传入其地址(&max、&min)。

  • 错误5:void指针直接进行算术运算,导致编译错误; 解决:将void指针强制转换为char*,再按字节偏移计算元素地址。

八、面试高频考点总结(必背)

  1. 顺序表高级功能:

    1. seqlist_search:返回元素地址,支持任意类型,依赖cmp函数;

    2. seqlist_modify_by_key:基于查找功能实现,代码复用性;

    3. seqlist_insert_sort:稳定排序,O(n²),适合小数据量。

  2. 快速排序:

    1. 核心:基准归位+分区+递归,平均O(nlogn),不稳定;

    2. 分区函数:双指针交替移动,完成基准归位和分区;

    3. 优化:基准选中间值,避免最坏情况。

  3. 折半查找:

    1. 前提:有序、顺序存储,O(logn);

    2. 中间下标计算:left + (right - left)/2,避免溢出;

    3. 循环条件:left ≤ right。

  4. 作业相关(面试可能手写):

    1. 顺序表倒序:双指针法,原地逆置,O(n);

    2. 参数回填:C语言无多返回值,用指针实现多结果输出(核心考点)。

  5. 通用化实现:

    1. void*指针:支持任意数据类型;

    2. 函数指针:自定义比较、打印规则,提高代码复用性和灵活性。

九、学习建议(补充)

  1. 代码一定要动手敲:本文所有代码均可直接运行,建议逐行敲一遍,理解每一步的原理,尤其是函数指针、void*指针、动态内存管理的细节;

  2. 多调试:遇到错误不要慌,通过调试逐步排查,重点关注指针地址、变量值的变化,理解排序、查找的执行过程;

  3. 多练习:尝试将int类型测试改为结构体类型(如学生结构体),验证通用接口的正确性,加深对void*指针和函数指针的理解;

  4. 牢记面试考点:本文总结的面试考点,务必熟练掌握,尤其是快速排序、折半查找的原理和时间复杂度,顺序表的通用化实现。

📌 本文所有代码均经过严格测试,可直接复制发布CSDN,收藏起来,后续学习链表、栈、队列时可无缝衔接。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询