C 语言算法

2024年8月28日 | 阅读16分钟

算法是一系列按预定顺序执行的指令,以解决问题或完成工作。函数是可以在程序其他部分调用和执行的代码块。

一套用于解决问题或执行特定活动的指令。在计算机科学中,算法用于各种操作,从基本数学运算到复杂的数据处理。

C语言中常用的算法之一是排序算法。排序算法按照特定顺序(例如数字顺序或字母顺序)排列一组项目。

排序算法有很多种,每种都有其优缺点。C语言中最常见的排序算法有快速排序、归并排序和sort(通常指标准库函数)。

C语言的一个关键特性是支持指针。这使得对数组、队列等数据结构进行高效操作成为可能。这使其适合实现需要复杂数据操作的算法,例如排序和算法搜索。

在C语言中实现的著名软件库的例子之一是标准模板库(STL)。该库提供了用于排序、搜索和操作数据结构等任务的多种算法。

算法的特性

它定义了算法的几个重要特性,包括:

  • 输入:算法必须接收可以表示为值或数据的输入。
  • 输出:算法应产生一些输出。它可以是问题的一个结果,也可以是为解决问题而设计的解决方案。
  • 清晰性:算法必须精确定义,使用清晰无歧义的指令,以便计算机或其他系统能够准确地遵循。
  • 有限性:算法需要有限的步骤。这意味着它应该在执行一定数量的命令后退出。
  • 有效性:算法必须是有效的。换句话说,它应该能够在合理的时间内为算法旨在解决的问题产生一个解决方案。
  • 有效性:算法必须是有效的,这意味着它必须能够在合理的时间内为它旨在解决的问题产生一个解决方案。
  • 通用性:算法必须是通用的,这意味着它可以应用于广泛的问题,而不是特定于单个问题。

算法分析

算法分析是评估算法在效率、复杂性和其他标准方面的性能的过程。通常,这样做是为了评估多种算法并为特定问题或软件选择最佳解决方案。

算法分析通常涉及衡量其时间和空间复杂度。

正如空间复杂度描述了所需的内存或磁盘空间一样,时间复杂度描述了算法执行任务所需的时间。

分析算法时间复杂度的不同方法有:大O表示法和Omega表示法。大O符号提供算法时间复杂度的上界,而Omega符号提供下界。

除了衡量时间和空间复杂度之外,算法分析还包括稳定性、并行性和可伸缩性等其他标准。

  1. 稳定性:- 这指的是算法保持数据集中元素相对顺序的能力。
  2. 并行性:- 这指的是跨多个处理器并行执行操作的能力。
  3. 可伸缩性:- 另一方面,它指的是算法处理大量数据和其他输入的能力。

它们包括两种分析方法。

它们是:-

  1. 先验分析。
  2. 后验分析。

先验分析

先验是一种算法分析方法,它侧重于在不实际执行算法的情况下,根据其数学特性来估计算法的性能。

这种方法允许您在无需实现和运行算法的情况下分析算法和其他指标的时间和空间复杂度。

后验分析

另一方面,后验分析是一种算法分析方法,它实际执行算法并衡量其性能。

这种方法提供了关于算法性能更准确和详细的信息,但需要实现和执行算法。

算法复杂度

算法复杂度是衡量算法效率和性能的度量。算法通常根据解决问题或实现特定目标所需的时间和空间来评估。

算法复杂度中使用两个因素。

它们是:-

  1. 时间因子。
  2. 空间因子。

时间因子

  • 算法完成任务所需的时间称为时间复杂度。它通常通过算法必须执行的运算或步骤数量来衡量,以解决问题。
  • 算法的时间复杂度很重要,因为它决定了其执行时间,并可能对程序和系统性能产生重大影响。
  • 算法的时间复杂度可以使用大O表示法来表示,这是一种表示算法时间复杂度上界的方法。
  • 时间复杂度为O(n)的算法意味着运行该算法所需的时间与输入数据的大小(n)成正比。
  • 其他常见的时间复杂度有O(n^2)二次复杂度,O(log n)对数复杂度。

空间分析

  • 另一方面,空间复杂度是指执行算法所需的内存或存储空间量。
  • 这一点很重要,因为它决定了运行算法所需的资源数量,这会影响您的应用程序或系统的整体性能。
  • 如果算法的空间复杂度为O(n),则它使用的内存量会随着输入大小线性增长。
  • 如果算法的空间复杂度为O(1),则它使用的内存量是固定的,与输入大小无关。

如何编写算法

1. 首先定义算法要解决的问题。

例如,假设我们要编写一个算法来查找数字列表中最大的值。

2. 将问题分解成更小、可管理的步骤。

  • 将“max”变量初始化为列表中的第一个值。
  • 对于列表中的每个后续值,将其与“max”进行比较。
  • 如果该值大于“max”,则将“max”设置为该值。
  • 继续执行此操作,直到列表中的每个值都已比较。
  • 返回最终的“max”值。

3. 用伪代码或编程语言编写算法。

伪代码算法

4. 测试算法以确保其正确且高效。

您可以通过输入不同的数字列表并验证它是否返回了正确的最大值来测试算法。您还可以分析算法的时间复杂度,以确定它在处理大型输入时的表现如何。

示例:-

输入:[1, 5, 2, 7, 3]

输出:7。

解释:7是列表中最大的值。

5. 优化算法。

寻找优化算法的方法,使其更快、更高效。这可能涉及修改伪代码或实现更有效的数据结构或算法。

算法的基本书写

示例:- 两个整数相加。

步骤 1 - 开始

步骤 2 - 声明三个整数a,b,c

步骤 3 - 定义a和b的值

步骤 4 - 添加a和b的值

步骤 5 - 将步骤4的输出保存在c中

步骤 6 - 打印c

步骤 7 - 停止

C语言中使用的算法类型。

1. 排序算法

C语言提供了丰富的数据类型和运算符,可用于实现各种排序算法,如冒泡排序、插入排序和快速排序。

这些算法在许多应用中都很有用,因为它们可用于对不同大小和类型的数据进行排序。

有不同的排序算法。

它们是:-

(一)冒泡排序:一种简单的排序算法,它反复比较相邻的元素,如果顺序不正确则交换它们。

冒泡排序算法如下:-

  1. 从一个未排序的元素列表开始。
  2. 比较列表中的前两个元素。如果第一个元素大于第二个元素,则交换它们。
  3. 移至下一对元素并重复步骤 2,直到到达列表末尾。
  4. 对于列表中的每个元素,再次重复步骤 2 和 3。这称为趟。
  5. 对整个列表重复步骤 2-4。随着趟的重复,元素将“冒泡”到其在已排序列表中的正确位置。
  6. 一旦一趟完成且没有发生交换,列表就已排序,算法可以停止。
  7. 返回最终排序的列表。

(二)插入排序:一种排序方法,它一次创建一个已排序的列表,将每个元素放入适当的位置。

插入排序算法如下:-

  1. 初始化一个空的已排序列表和一个要排序的元素的未排序列表。
  2. 从未排序列表中取出第一个成员,并将其放入已排序列表中的适当位置。
  3. 对未排序列表中的每个后续元素重复步骤 2。
  4. 将当前元素与已排序列表中的元素进行比较,从紧邻其左侧的元素开始。
  5. 如果当前元素小于其左侧的元素,则交换这两个元素。
  6. 如果当前元素大于其左侧的元素,则将其插入已排序列表中的正确位置。
  7. 对未排序列表中的每个后续元素重复步骤 4-6。
  8. 处理完所有元素后,已排序列表将包含按正确顺序排列的所有元素。
  9. 排序过程完成。

(三)选择排序:一种排序方法,它总是从无序列表中提取最小的元素开始构建已排序列表。

选择排序算法如下:-

  1. 开始时,将列表的第一个元素设置为最小元素。
  2. 遍历列表中的其余项,将每个项与当前最小值进行比较。
  3. 如果找到一个元素比现有元素小,则将其设置为新的最小值。
  4. 当到达列表末尾时,将当前最小值与列表的第一个元素进行交换。
  5. 对于列表中剩余的未排序部分,重复步骤 2-4,但从列表的第二个元素开始(因为第一个元素已排序)。
  6. 以这种方式继续对列表进行排序,直到它完全排序。

(四)快速排序:一种分而治之的排序算法,它选择一个基准元素,并将列表分成两部分:一部分包含小于基准的元素,另一部分包含大于基准的元素。然后,重复对子列表进行排序,直到整个列表排序完成。

快速排序算法如下:-

  1. 从列表中选择一个基准元素。这通常是第一个元素,但也可以是随机元素或列表的中位数。
  2. 将列表分成两个子列表:一个包含小于基准的元素,一个包含大于基准的元素。
  3. 使用相同的方法递归地对包含小于基准的元素的子列表进行排序。
  4. 使用相同的方法递归地对大于基准的元素的子列表进行排序。
  5. 将排序后的子列表与基准元素连接起来,形成一个完全排序的列表。
  6. 返回完全排序的列表。

(五)归并排序:分而治之的排序算法将列表分成两半,对每一半进行排序,然后将两半按排序顺序合并。

归并排序算法

  1. 将列表分成两个子列表:一个包含小于基准的元素,一个包含大于基准的元素。
  2. 通过迭代地合并子列表,直到只剩下一个子列表,从而产生一个新的已排序子列表。这将是您的已排序列表。
  3. 合并两个子列表的步骤:-
  4. 创建一个空列表来保存已排序的元素。
  5. 比较每个子列表的第一个元素。
  6. 将较小的元素添加到新列表中,并将其从父子列表中移除。
  7. 重复步骤 2 和 3,直到一个列表完全为空。
  8. 将其他子列表中剩余的元素添加到新列表中。
  9. 用新排序的列表替换合并的子列表。
  10. 重复此过程,直到所有子列表合并为一个已排序列表。

(六)堆排序:一种使用称为堆的数据结构来排序元素的排序算法。

这是分类算法

  1. 构建最大堆:从第一个非叶节点开始,将每个节点与其子节点进行比较,并将节点替换为其子节点中最大的节点,以满足最大堆属性。
  2. 交换根与最后一个元素:将根(最大元素)与堆栈中的最后一个元素进行交换。
  3. 堆叠其余元素。从根开始,每个节点都与其子节点进行比较,将节点与其较大的子节点进行交换,直到满足最大堆属性。
  4. 对新堆叠的元素重复步骤 2 和 3,但最后一个元素除外(因为它已处于正确位置)。
  5. 重复此过程,直到堆中只剩下一个元素。现在它已排序。
  6. 堆化下移:从根节点开始,它将其与子节点进行比较,并与两者中较大的那个交换,直到满足最大堆属性。
  7. 堆化上移:从堆的最后一个元素开始,将其与其父节点进行比较,并与其父节点交换,以满足最大堆属性。

(七)基数排序:一种根据元素的二进制表示的数字或位来排序元素的排序算法。

基数排序算法如下:-

  1. 确定输入列表的最大元素包含多少位。
  2. 初始化一个变量,例如“digitPlace”,设置为 1,表示当前数字位。
  3. 为从 0 到 9 的每个可能的数字值创建一个空列表。
  4. 遍历输入列表,并根据当前数字位的值将每个元素添加到相应的列表中。
  5. 将所有列表连接起来,形成一个新列表,顺序为数字列表。
  6. 将 digitPlace 乘以 10,以移至下一个数字位。
  7. 对每个数字位重复步骤 4-6,直到考虑完最大元素中的所有数字。
  8. 最终列表将按元素数字的升序排序。
  9. 返回最终排序的列表。

2. 搜索算法

C语言还提供了实现各种搜索算法所需的工具,例如线性搜索和二分搜索。这些算法可以快速查找数据集中特定的项,使其适用于各种应用。

有许多类型的搜索算法。

它们是:-

(一)线性搜索:一种基本的搜索算法,它逐个检查列表中的每个项,直到找到所需的项。

线性搜索算法:-

  1. 定义算法的输入:线性搜索算法的输入是一个元素列表(或数组)和一个我们要搜索的目标元素。
  2. 初始化一个名为“index”的变量为 -1:此变量将用于存储目标元素的索引(如果找到)。
  3. 循环遍历元素列表:从第一个元素开始,逐个检查列表中的每个元素。
  4. 将当前元素与目标元素进行比较以进行评估:如果当前元素与目标元素相同,则将当前元素的索引保存在 index 变量中并退出循环。
  5. 返回目标元素的索引:循环完成后,返回 index 变量中存储的值。如果未找到目标元素,则 index 的值为 -1。

(二)二分搜索:一种搜索算法,它通过将列表分成两半来工作,并在更可能包含该元素的那个半部分进行搜索。

二分搜索算法:-

  1. 输入:一个包含 n 个元素的已排序列表和一个目标元素 x。
  2. 初始化变量:将 low 索引设置为 0,high 索引设置为 n-1,mid 设置为 (low+high)/2。
  3. 开始循环:当 low 索引小于或等于 high 索引时,重复以下步骤。
  4. 将 mid 元素与 x 进行比较:如果 mid 元素等于 x,则返回 mid 索引。
  5. 更新 low 或 high 索引:如果 x 大于 mid 元素,则将 low 索引设置为 mid + 1。否则,将 high 索引设置为 mid - 1。
  6. 更新 mid 索引:Mid = (low+high)/2。
  7. 循环结束:如果 low 索引大于 high 索引,则 x 不在列表中,算法返回失败。
  8. 输出:x 在列表中的索引或失败消息。

(三)深度优先搜索:一种搜索算法,它尽可能深入地探索每个分支,然后再回溯。

深度优先搜索遵循以下准则:

  1. 选择图的起始顶点或节点开始。
  2. 在第一个顶点上加上访问标记。
  3. 将起始顶点直接放入堆栈。
  4. 直到堆栈为空,重复以下操作:-
    • 弹出堆栈顶部的顶点。
    • 将已弹出顶点的每个未访问邻居标记为已访问并放入堆栈。
  5. 继续此过程,直到访问图中的所有顶点。
  6. 一旦所有顶点都已被访问,算法就完成了,并且图已执行深度优先搜索。

(四)广度优先搜索:一种搜索算法,它在进入下一级别之前会探索一个节点的所有邻居。

广度优先搜索算法如下:-

  1. 从根节点或初始状态开始。
  2. 将根节点添加到队列中。
  3. 检查队列是否为空;如果是,则终止算法。
  4. 从队列中取出第一个元素并将其标记为已访问。
  5. 通过将其所有未访问的邻居添加到队列来扩展当前节点。
  6. 重复步骤 3 到 5,直到找到目标节点或队列为空。
  7. 如果找到目标节点,则返回从初始状态到目标状态的路径。
  8. 如果队列为空,则终止规则集并报告未找到目标状态。

(五)插值查找:一种利用被搜索元素的值来估计索引位置的搜索算法。

数组是均匀分布的很重要。否则,它就是一种算法。

它的效果如预期。

算法可总结如下:

  1. 获取输入列表和要搜索的键值。
  2. 将 lower 和 upper 变量初始化为列表的第一个和最后一个索引。
  3. 如果 lower 值小于或等于 higher 值,则:-
    1. 使用以下公式计算估计位置
      pos = low + ((high - low) / (arr[high] - arr[low])) * (x - arr[low])。
    2. 如果估计位置值是键值,则返回该位置。
    3. c) 如果估计位置值小于键值,则将其 lower 设置为
      位置 + 1。
    4. d) 如果估计位置的值大于键值,则将其 upper 设置为位置 - 1。
  4. 如果未找到键值,则返回 -1,表示该值不在列表中。

(六)跳跃搜索:一种搜索方法,它以恒定步长遍历列表,直到找到相关元素或确定它不再存在。

跳跃搜索算法如下:

  1. 首先,将跳跃大小设置为数组元素数量的平方根。
  2. 设置一个名为“current”的变量,指向数组的第一个元素。
  3. 通过以跳跃大小为增量跳跃来遍历数组,同时增加一个名为“jump”的变量。
  4. 如果当前元素小于目标元素,则移至下一个跳跃。
  5. 如果当前元素大于目标元素,则在当前元素和前一个跳跃元素之间执行线性搜索以查找目标元素。
  6. 如果在数组中未找到目标元素,则返回 -1,表示它不在数组中。
  7. 如果找到元素,则返回该元素在数组中的索引。

3. 图算法

C语言对指针以及数组和链表等数据结构的支持使其非常适合实现处理图的算法,例如在图的两个节点之间找到最短路径。

有不同类型的图算法。

它们是:-

  1. Dijkstra 算法:一种通过不断更新每个节点的到源节点的距离来查找图中两个节点之间最短路径的算法。
  2. A* 算法:一种不断更新图中每个节点到目标节点的最短距离,以确定它们之间最短路径的方法。
  3. Prim 算法:一种用于找出加权连通图中最小生成树的方法。
  4. Kruskal 算法:一种用于找出连通加权图中最小生成树的方法。
  5. Bellman-Ford 算法:一种算法,即使在图具有负边权重的情况下,也能显示特定源节点与网络中其他所有节点之间的最短路径。

4. 加密算法

C语言支持低级操作和高效的数据操作,使其非常适合实现密码学中使用的算法,例如数据加密和解密算法。

有不同类型的加密算法。

它们是:-

  1. 哈希算法:这些算法从任意大小的输入生成固定大小的输出(哈希)。示例包括 MD5、SHA-1 和 SHA-2。
  2. 对称密钥算法:这类算法的加密和解密步骤使用相同的私钥。AES、DES 和 Blowfish 是其中的一些例子。
  3. 非对称密钥算法:公钥和非公钥使用这些方法作为独立的密钥进行加密和解密。一些例子包括 RSA、ECC 和 DSA。
  4. 密钥交换算法:这些算法允许双方在不安全的通道上安全地交换密钥。例如,我们可以提到 Diffie-Hellman 和椭圆曲线 Diffie-Hellman。

算法的优点

算法有很多优点。

它们是:-

  1. 速度和效率:算法可以快速准确地处理大量数据,使其适用于对人类来说耗时过长或容易出错的任务。
  2. 一致性:算法遵循一组预定的指导方针。它可以产生一致的结果,而不受个人偏见和情绪的影响。
  3. 自动化:算法可以自动执行任务,使人们能够专注于更复杂或更具创造性的任务。
  4. 提高准确性:算法通常比人类能够达到更高的准确性水平,尤其是在处理大量数据时。
  5. 更好的决策制定:算法通过分析数据并识别对人类来说不那么明显的数据模式和趋势,帮助我们做出更明智、更客观的决策。
  6. 可伸缩性:算法可以轻松地进行扩展或缩减,以满足不断变化的需求和工作负载。

算法的缺点

算法对于编程非常有用,但算法也有缺点。

它们是:-

  1. 范围有限:算法只能解决其范围内的问题,可能无法解决复杂或抽象的问题。
  2. 偏见:算法可能会延续和加强训练数据中的偏见,导致不公平的结果。
  3. 透明度不足:许多算法隐藏了它们得出结论的过程。这可能难以思考或检查结果。
  4. 对数据准确性的依赖:规则集的正确性在很大程度上取决于用于指令的数据的准确性和适用性。不准确或错误的数据可能导致不准确的结果。
  5. 适应性有限:算法旨在遵循指导方针,不会适应不断变化的环境和条件。