尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

一元多项式计算:链表实现与源码解析

一元多项式计算:链表实现与源码解析 简介这份资源面向学习数据结构课程的学生与需要完成课程设计的读者围绕一元多项式计算这一经典题目提供完整的课程设计报告文档与可直接运行的源码。内容涵盖不带头结点单链表的存储结构设计、主程序功能结构图、多项式按指数降序排列的排序算法以及加法和减法运算的具体实现并附有读取、输出、菜单等函数说明与测试数据分析。资源包共1个doc文件约393KB文档中整合了需求分析、概要设计、详细设计、源代码与运行结果便于对照理解算法流程与代码逻辑。目前已有1507人学习下载适合需要提交课设报告、复习链表操作或参考多项式运算实现思路的读者可帮助快速掌握从结构体定义到加减法合并同类项的完整设计方法。1. 一元多项式计算到底在算什么从链表节点到可运行源码很多人第一次看到「数据结构一元多项式计算」这个题目脑子里浮现的是数学课上的合并同类项觉得无非就是把 $3x^22x1$ 和 $x^2-4x5$ 加起来。但真正动手写代码时才会发现难点根本不在数学而在于怎么用数据结构把「系数 指数」这一对关系存下来并且让加法、减法、乘法都能高效跑通。这也是为什么它常年出现在数据结构实验报告和课程设计里王道408、严蔚敏数据结构C语言版这类教材都拿它当链表的经典应用题。这篇要讲清楚的是一元多项式计算用链表怎么实现、源码怎么组织、文档该写什么、拿到代码后怎么直接跑起来。适合正在做数据结构实验、课程设计或者想找一个能直接运行的链表实战案例的人。核心思路是用带头结点的单链表存储多项式每个节点存系数 coef 和指数 exp加法走归并思路乘法走逐项相乘再合并。下面从原理到源码一步步拆开最后给一份能直接编译运行的完整实现。2. 用带头结点单链表存多项式节点设计与归并加法2.1 为什么选链表而不是数组存一元多项式最直觉的做法是开一个数组下标当指数值当系数。比如coef[5] 3就表示 $3x^5$。这个方案在指数密集、范围小的时候很好用但一旦遇到 $3x^{1000}2x^2$ 这种稀疏多项式数组就要开一千多个位置绝大部分是零空间浪费严重。而且数组长度固定动态插入新项很麻烦。链表的好处是只存非零项每一项占一个节点指数跨度再大也不影响空间。代价是访问第 k 项要遍历但对于多项式加法这种需要顺序扫描的场景链表反而更自然。常见做法是带头结点的单链表头结点不存数据只用来统一插入和删除的边界处理避免对第一个节点做特殊判断。节点结构一般这样定义typedef struct PNode { float coef; // 系数 int exp; // 指数 struct PNode *next; // 指向下一项 } PNode, *Polynomial;系数用 float 是为了支持小数系数如果只做整数运算用 int 也行。指数用 int允许为负表示分式项。这里有个约定链表按指数递减或递增有序排列且不含系数为 0 的项。这个约定是后面所有算法能高效运行的前提插入和合并时都要维护它。2.2 归并加法两个有序链表怎么合并加法本质上就是合并同类项。因为两个多项式都按指数有序所以可以用类似归并排序的双指针思路一次遍历搞定时间复杂度 O(mn)。规则很简单比较两个当前节点的指数。指数相等就把系数相加和不为零就保留为零就丢弃因为约定不含零项指数不等就把指数大的那个节点接到结果链表上。谁大接谁保证结果仍然有序。Polynomial AddPolynomial(Polynomial A, Polynomial B) { PNode *pa A-next; // 跳过头结点 PNode *pb B-next; Polynomial C (Polynomial)malloc(sizeof(PNode)); // 结果头结点 C-next NULL; PNode *pc C; // 尾指针始终指向结果链表最后一个节点 while (pa pb) { if (pa-exp pb-exp) { float sum pa-coef pb-coef; if (sum ! 0) { // 系数和为0则该项消失 PNode *node (PNode)malloc(sizeof(PNode)); node-coef sum; node-exp pa-exp; node-next NULL; pc-next node; pc node; } pa pa-next; pb pb-next; } else if (pa-exp pb-exp) { // A的指数大先接A PNode *node (PNode)malloc(sizeof(PNode)); *node *pa; // 复制数据 node-next NULL; pc-next node; pc node; pa pa-next; } else { // B的指数大先接B PNode *node (PNode)malloc(sizeof(PNode)); *node *pb; node-next NULL; pc-next node; pc node; pb pb-next; } } // 把剩余部分直接接上 while (pa) { PNode *node (PNode)malloc(sizeof(PNode)); *node *pa; node-next NULL; pc-next node; pc node; pa pa-next; } while (pb) { PNode *node (PNode)malloc(sizeof(PNode)); *node *pb; node-next NULL; pc-next node; pc node; pb pb-next; } return C; }这段代码的关键点有三个。第一用尾指针 pc 避免每次插入都从头遍历把整体复杂度控制在 O(mn)。第二系数和为 0 时直接跳过不生成节点这样结果里不会出现0x^3这种脏数据。第三剩余部分用 while 循环逐个复制而不是直接pc-next pa因为直接接上会共享节点后续如果释放或修改 A、B 会出问题。如果确定 A、B 用完就丢可以直接接省一次复制。参数上A 和 B 都是带头结点的多项式链表函数返回一个新的带头结点链表 C。调用方负责释放 C 的内存。指数相等判断用因为指数是整数不存在浮点误差问题系数比较用! 0时要注意 float 精度如果系数来自大量运算建议加一个极小阈值如fabs(sum) 1e-6再保留。2.3 插入操作怎么维护有序性加法是合并两个已有链表但实际使用中经常需要动态插入一项比如用户输入或者乘法过程中产生新项。插入的核心是找到第一个指数小于等于待插入项的位置然后插在它前面。如果指数相等就合并系数合并后为零则删除该节点。void InsertTerm(Polynomial P, float coef, int exp) { if (coef 0) return; // 零项不插入 PNode *pre P; // 前驱指针 PNode *cur P-next; while (cur cur-exp exp) { // 找插入位置 pre cur; cur cur-next; } if (cur cur-exp exp) { // 指数相同合并 cur-coef coef; if (cur-coef 0) { // 合并后为零删除节点 pre-next cur-next; free(cur); } } else { // 新项插入 PNode *node (PNode)malloc(sizeof(PNode)); node-coef coef; node-exp exp; node-next cur; pre-next node; } }这里 pre 和 cur 双指针的用法是链表插入的标准套路pre 始终是 cur 的前一个节点。循环条件是cur-exp exp意味着跳过所有指数比待插入项大的项停在第一个指数小于等于 exp 的位置。如果停下的位置指数正好相等就合并否则插在 pre 和 cur 之间。注意合并后系数为零要 free 掉节点否则链表里会残留零项破坏「不含零项」的约定后续加法就会出错。这个插入函数是后面乘法的基础乘法就是反复调用插入把每一项乘积塞进结果链表。单次插入最坏 O(n)n 项乘法整体 O(n²)对于课程设计规模完全够用。3. 乘法与求值从 O(n²) 逐项相乘到秦九韶优化3.1 逐项相乘再插入最稳的乘法实现多项式乘法没有加法那么优雅的归并写法因为两个多项式相乘会产生 m×n 个中间项而且指数是两两相加顺序是乱的。最稳妥的做法就是双重循环把 A 的每一项和 B 的每一项相乘得到新系数和新指数然后调用 InsertTerm 插入结果链表。Polynomial MultiplyPolynomial(Polynomial A, Polynomial B) { Polynomial C (Polynomial)malloc(sizeof(PNode)); C-next NULL; PNode *pa A-next; while (pa) { PNode *pb B-next; while (pb) { float coef pa-coef * pb-coef; int exp pa-exp pb-exp; InsertTerm(C, coef, exp); // 插入时自动合并同类项 pb pb-next; } pa pa-next; } return C; }这段代码短但信息量大。外层遍历 A 的每一项内层遍历 B 的每一项系数相乘、指数相加这是多项式乘法的数学定义。关键在于 InsertTerm 承担了合并同类项的工作所以即使中间产生大量同指数项最终结果也是干净的。时间复杂度 O(m×n×k)k 是结果链表长度最坏 O(n³)但对于实验规模几十项完全能接受。参数上要注意如果 A 或 B 为空只有头结点双重循环不会执行直接返回空多项式这是正确的。系数相乘可能产生浮点误差如果对精度敏感可以在 InsertTerm 里用阈值判断零项。另外乘法不修改 A 和 B所以可以安全地重复调用。3.2 求值代入 x 算结果多项式求值就是给定 x算出整个表达式的值。最直接的做法是遍历链表对每一项算coef * pow(x, exp)然后累加。但 pow 函数对整数指数效率不高而且负指数要特殊处理。double EvaluatePolynomial(Polynomial P, double x) { double result 0.0; PNode *p P-next; while (p) { double term p-coef; int e p-exp; if (e 0) { for (int i 0; i e; i) term * x; // 正指数累乘 } else { for (int i 0; i -e; i) term / x; // 负指数累除 } result term; p p-next; } return result; }用累乘代替 pow 是为了避免引入 math.h 的依赖也避免 pow 在整数指数下的精度问题。负指数用除法处理注意 x 不能为 0。如果多项式按指数递减排列且指数范围很大这个逐项累乘会重复计算很多次 x 的幂。优化思路是用秦九韶Horner法则但 Horner 要求指数连续稀疏多项式需要先补齐缺项反而可能更慢。对于稀疏多项式逐项计算是合理选择。如果指数都是非负整数且范围不大可以预计算 x 的幂表把 O(n×e) 降到 O(ne)。这个优化在指数密集时效果明显稀疏时收益有限。3.3 减法与复制两个容易被忽略的辅助函数减法本质上是把 B 的每一项系数取反再做加法。但直接修改 B 会破坏原数据所以要先复制一份再取反。Polynomial CopyPolynomial(Polynomial P) { Polynomial C (Polynomial)malloc(sizeof(PNode)); C-next NULL; PNode *pc C; PNode *p P-next; while (p) { PNode *node (PNode)malloc(sizeof(PNode)); node-coef p-coef; node-exp p-exp; node-next NULL; pc-next node; pc node; p p-next; } return C; } Polynomial SubtractPolynomial(Polynomial A, Polynomial B) { Polynomial Bcopy CopyPolynomial(B); PNode *p Bcopy-next; while (p) { p-coef -p-coef; // 系数取反 p p-next; } Polynomial C AddPolynomial(A, Bcopy); FreePolynomial(Bcopy); // 释放临时副本 return C; }复制函数是深拷贝每个节点都新分配内存这样修改副本不影响原链表。减法先复制 B、取反、再调用加法逻辑清晰且不会污染输入。注意最后要释放 Bcopy否则每次减法都泄漏一份 B 的内存。这种「复制-修改-计算-释放」的模式在链表操作里很常见血泪经验是千万别图省事直接改原链表否则调试时数据莫名其妙变了找半天找不到原因。FreePolynomial 就是遍历链表逐个 free包括头结点。这个函数看着简单但漏写就是内存泄漏长时间运行的程序会慢慢吃光内存。4. 避坑与排查一元多项式链表最容易翻车的五个地方4.1 插入后链表不再有序加法结果错乱现象加法结果里出现重复指数项或者顺序乱了比如 $3x^2$ 出现在 $x^5$ 前面。原因InsertTerm 的循环条件写反了或者插入位置找错。常见的是把cur-exp exp写成cur-exp exp导致链表按递增排列而加法假设递减两边不一致就乱套。解决统一约定。要么全按指数递减要么全按递增所有函数保持一致。调试时写一个 PrintPolynomial 函数每次插入后打印链表肉眼检查顺序。如果顺序不对先确认插入函数的循环方向再确认初始化时是不是按约定顺序建的链表。4.2 系数合并后为零没删节点结果里全是零项现象两个多项式相加某些项系数抵消了但结果里还留着0x^3这种项打印出来很难看后续乘法还会多算。原因加法里指数相等时只做了sum pa-coef pb-coef没判断 sum 是否为零就直接生成节点。或者 InsertTerm 里合并后没检查cur-coef 0。解决所有产生新系数的地方都加零判断。加法里if (sum ! 0)才生成节点插入里合并后if (cur-coef 0)就删除节点。浮点系数建议用fabs(sum) 1e-6代替sum ! 0避免 0.10.2-0.3 这种精度问题导致该删的没删。4.3 直接接剩余链表导致双重释放现象程序运行到释放内存时崩溃报 double free 或 invalid free。原因加法里剩余部分写成pc-next pa直接共享了 A 的节点。后来释放结果链表 C 时把这些节点 free 了再释放 A 时又 free 一次同一块内存释放两次就崩。解决要么像本文代码那样逐个复制节点要么在接上之前把 A、B 的头结点断开明确所有权转移。课程设计里推荐复制逻辑简单不易错。如果追求性能要直接接就在文档里写清楚「调用 AddPolynomial 后 A 和 B 不可再使用」并且调用方不要再释放 A、B。4.4 乘法结果指数溢出或系数精度丢失现象大指数相乘后 exp 变成负数或者系数算出来是 1.9999999 而不是 2。原因exp 用 int两个大指数相加可能溢出。系数用 float多次相乘累积误差。解决指数用 long long 如果可能超过 int 范围或者提前检查pa-exp pb-exp是否溢出。系数如果对精度要求高用 double 代替 float并且在最终输出时做四舍五入。课程设计规模一般不会溢出但写文档时要说明数据范围限制。4.5 输入解析把负号和减号搞混现象输入-3x^22x-1时解析出错把减号当成了负系数的一部分或者把x^-2的负指数解析错。原因手写解析器时没有区分「二元减号」和「一元负号」或者正则没考虑负指数。解决解析时先按项拆分每项用正则([-]?\d*\.?\d*)x\^?([-]?\d)?提取系数和指数。注意x单独出现时系数是 1-x系数是 -1x^2指数是 2x^-2指数是 -2。建议先写测试用例覆盖这些边界再写解析逻辑。如果时间紧直接让用户按「系数 指数」成对输入绕开解析这个坑。5. 源码组织与文档写法让代码能直接跑起来5.1 单文件还是多文件课程设计的取舍课程设计通常要求「文档 源码可以直接运行」。最省事的组织方式是单文件polynomial.c所有函数放一起main里写测试用例编译一条命令搞定。这种方式适合提交作业评审老师打开就能编译。如果想让代码更专业可以拆成三个文件polynomial.h放结构体定义和函数声明polynomial.c放实现main.c放测试。编译用gcc main.c polynomial.c -o poly。拆文件的好处是接口清晰文档里可以直接引用头文件说明 API。缺点是编译命令变长新手容易漏文件。我一般推荐单文件因为一元多项式计算本身代码量不大三百行左右拆开反而增加理解成本。文档里把每个函数的用途、参数、返回值写清楚比拆文件更有价值。5.2 文档该写什么实验报告的四个必备部分数据结构实验报告的常见结构是需求分析、数据结构设计、算法设计、测试与结果。对应到一元多项式计算需求分析写清楚支持哪些运算加、减、乘、求值输入输出格式是什么数据范围限制。数据结构设计画节点结构图说明为什么选链表、为什么带头结点、为什么按指数有序。算法设计逐个函数讲思路加法重点讲归并乘法重点讲双重循环加插入合并配上伪代码或流程图。测试与结果给至少三组测试用例包括普通情况、系数抵消、空多项式把输入和输出都贴出来。文档里最容易缺的是边界情况说明。比如空多项式加空多项式返回什么系数全抵消返回什么指数为负怎么处理。这些写清楚评审时能加分自己调试时也有依据。5.3 编译运行与测试用例拿到源码后直接编译运行gcc polynomial.c -o polynomial -lm ./polynomial-lm是链接数学库如果代码里没用 pow 可以不加。运行后按提示输入两个多项式程序输出加法、减法、乘法和求值结果。测试用例建议覆盖这几组用例多项式 A多项式 B验证点普通相加3x^22x1x^2-4x5同类项合并结果 4x^2-2x6系数抵消x^21-x^22x^2 项消失结果 2空多项式03x1返回 3x1不崩溃乘法x1x-1结果 x^2-1验证平方差负指数x^-11x^-1-1结果 2x^-1验证负指数处理每组用例在文档里贴出实际运行截图或文本输出证明代码真的能跑。这一步很多人偷懒不写但评审老师最看重的就是「你说能跑证据呢」。5.4 一个可直接运行的完整 main 示例#include stdio.h #include stdlib.h // 此处省略 PNode 定义和 Add/Insert/Multiply 等函数按前文实现 void PrintPolynomial(Polynomial P) { PNode *p P-next; if (!p) { printf(0\n); return; } int first 1; while (p) { if (!first p-coef 0) printf(); if (p-exp 0) printf(%.2f, p-coef); else if (p-exp 1) printf(%.2fx, p-coef); else printf(%.2fx^%d, p-coef, p-exp); first 0; p p-next; } printf(\n); } int main() { Polynomial A CreatePolynomial(); Polynomial B CreatePolynomial(); // 构造 A 3x^2 2x 1 InsertTerm(A, 3, 2); InsertTerm(A, 2, 1); InsertTerm(A, 1, 0); // 构造 B x^2 - 4x 5 InsertTerm(B, 1, 2); InsertTerm(B, -4, 1); InsertTerm(B, 5, 0); printf(A ); PrintPolynomial(A); printf(B ); PrintPolynomial(B); Polynomial sum AddPolynomial(A, B); printf(AB ); PrintPolynomial(sum); Polynomial diff SubtractPolynomial(A, B); printf(A-B ); PrintPolynomial(diff); Polynomial prod MultiplyPolynomial(A, B); printf(A*B ); PrintPolynomial(prod); printf(A(2) %.2f\n, EvaluatePolynomial(A, 2.0)); FreePolynomial(A); FreePolynomial(B); FreePolynomial(sum); FreePolynomial(diff); FreePolynomial(prod); return 0; }CreatePolynomial 就是分配一个头结点并返回next置 NULL。PrintPolynomial 处理了正负号、指数为 0 和 1 的特殊显示输出更接近数学写法。main 里构造了两个多项式依次调用各运算并打印最后统一释放内存。这个结构可以直接作为课程设计的提交版本把函数实现补全就能跑。编译时如果报undefined reference to pow加上-lm。如果报段错误优先检查 CreatePolynomial 是否分配了头结点、InsertTerm 是否在空链表上正确工作。调试链表问题最有效的习惯是在每个函数入口打印当前链表状态虽然土但能快速定位是哪一步把链表搞坏的。希望帮到你。本文还有配套的精品资源点击获取
返回列表