龙空技术网

大话计算机408考研:第2篇 数据结构与算法概述(下)

四零八研学社 45

前言:

而今姐妹们对“算法的重要特性是指”都比较关怀,各位老铁们都需要剖析一些“算法的重要特性是指”的相关知识。那么小编同时在网上搜集了一些关于“算法的重要特性是指””的相关知识,希望你们能喜欢,咱们快快来了解一下吧!

算法基本概念

算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,它代表着用系统的方法描述解决问题的策略机制。这些指令描述了一个计算,当其运行时能从一个初始状态和(可能为空的)初始输入开始,经过一系列有限而清晰定义的状态,最终产生输出并停止于一个终态。一个状态到另一个状态的转移不一定是确定的,随机化算法在内的一些算法包含了一些随机输入。

算法特性

算法的基本特性包括:

有穷性:这是指算法在执行时,其步骤必须是有限的,也就是说,算法必须能在执行有限个步骤之后终止。每个步骤都应在有限时间内完成,这是为了确保算法能在有限的时间内得出结果。确定性:算法的每一个步骤都必须是确切定义的,不能有歧义。这意味着算法的每一步都应该是清晰明确的,使得执行者能够准确无误地按照步骤进行。可行性:算法中的每一个运算都应在相应的计算装置上能实现,这要求算法中的运算必须是有效的,并且能通过有限次运算完成。输入:一个算法具有零个或多个输入,这些输入是算法执行时所需的初始量或被加工的对象。这些输入通常来自于特定的对象集合,它们为算法提供了起始条件或数据。输出:与输入相对应,算法具有一个或多个输出,这些输出是与算法处理的输入数据有特定关系的量。输出是算法执行后的结果,可能包括计算结果、状态变化或其他形式的信息。

在设计和实现算法时,需要充分考虑这些特性,以确保算法的正确性和效率。

算法设计要求

算法的设计要求主要涵盖以下几个方面:

正确性:算法应当满足以特定的“规则说明”方式给出的需求。这要求算法对于所有合法的输入数据都能得出满足要求的结果。正确性是算法设计的首要要求,它确保了算法能够按照预期的方式运行并产生正确的输出。健壮性:当输入的数据非法时,算法应当能够恰当地做出反应或进行相应处理,而不是产生错误的输出结果。健壮性要求算法能够处理异常情况,并在遇到错误输入时能够给出合理的反馈或采取适当的措施。可读性:算法应该易于人的理解,以便于交流和后续的维护。可读性好的算法通常具有清晰的逻辑结构和注释,能够使人更容易理解算法的工作原理和实现方式。高效性:算法需要在合理的时间内解决问题,最大限度地减少所需的时间和其他资源。高效性要求算法在设计时要考虑其执行效率,避免不必要的计算和资源消耗。可扩展性:算法需要具备可扩展性,以满足未来的需求。这要求算法能够适应不同规模和复杂度的问题,并能够灵活地适应未来的变化。模块化:算法应该能够在分离的模块中运行,并能够灵活地组合在一起。模块化设计有助于降低算法的复杂度,提高代码的可维护性和可重用性。可测试性:算法应该具有可测试性,能够使用标准数据集进行测试,以便计算结果能够被重复和验证。这有助于确保算法的可靠性和稳定性。安全性:算法必须确保不会破坏数据的机密性或完整性,不能泄露敏感数据。安全性是算法设计中不可忽视的重要方面,特别是在处理敏感信息或执行关键任务时。

这些要求共同构成了算法设计的核心原则,有助于设计出高质量、高效且可靠的算法。

算法表现形式

算法的表现形式主要可以分为以下几种:

自然语言描述:算法可以使用人类日常使用的自然语言(如中文或英文)进行描述。这种方式直观易懂,但可能不够精确,容易产生歧义。它通常用于算法思想的初步描述或向非专业人士解释算法。流程图:流程图是一种图形化的算法表示方式,它使用图形符号来表示算法中的各个步骤以及它们之间的逻辑关系。流程图直观易懂,能够清晰地展示算法的流程,但可能不够详细,无法完全替代文字描述。伪代码:伪代码是一种介于自然语言和编程语言之间的算法表示方式。它使用类似编程语言的语法和结构来描述算法,但又不受具体编程语言的限制。伪代码既保留了算法的逻辑结构,又具有一定的可读性,是算法设计和实现过程中常用的工具。程序代码:程序代码是使用具体编程语言实现的算法。它是算法在计算机上的最终表现形式,可以直接被计算机执行。程序代码需要遵循编程语言的语法规则,并且需要考虑到计算机的性能和资源限制。算法分析

算法分析是对一个算法在解决特定问题时所需的计算时间和存储空间进行定量的分析。其目的在于评估算法的效率和正确性,从而帮助人们更好地理解算法的优点和缺点,以及如何调整算法以提高其性能和效率。

算法分析可以从多个维度进行,其中最主要的两个方面是时间复杂度和空间复杂度。

时间复杂度:主要关注算法执行所需的时间,即算法执行过程中所需的基本操作次数。通过分析算法的时间复杂度,可以了解算法在不同规模数据下的运行效率,从而选择更适合特定问题的算法。空间复杂度:主要关注算法执行过程中所需的额外空间,包括算法在执行过程中临时占用的存储空间。空间复杂度的分析有助于优化算法的内存使用,避免不必要的空间浪费。

除此之外,还需要考虑算法的稳定性、鲁棒性、可扩展性等其他因素。这些因素同样影响算法在实际应用中的表现。

算法时间复杂度

算法时间复杂度表示算法输入值的规模与算法执行时间之间的关系。时间复杂度常用大O符号表示,这是一种表示法,用于描述算法执行时间随输入规模增长的上界。大O表示法提供的是一个渐近的上界,而不是确切的运行时间。

算法时间复杂度分析通常采用大O表示法,具体表达式如下:

T = O(f(n))

其中,

T 代表算法执行所需的时间(或更准确地说是执行时间的某个度量)O(f(n)) 是大O表示法,用于描述 T 的上界

注意,大O表示法并不关心算法在特定小规模输入下的实际性能,而是关注当输入规模变得非常大时算法的性能如何。同时,它忽略了低阶项和常数因子,因为这些在 n 趋近于无穷大时变得相对不重要。

例如,假设有一个算法,其执行时间可以表示为 T(n) = 3n^2 + 2n + 10。当分析这个算法的时间复杂度时,通常会忽略掉 2n10 这两项,因为它们相对于 3n^2 来说在 n 很大时的影响很小。因此,该算法的时间复杂度是 O(n^2)

常见大O分析法O(1)分析法

O(1)是最低的时间复杂度,表示算法的速度与输入数据的数量无关。无论输入数据是多少,算法的执行时间都是恒定的。

代码示例:

#include <stdio.h>  // 计算一个整数的平方int square(int n) {      return n * n;  }  int main() {      int num = 5;      printf("Square of %d is %d\n", num, square(num));      return 0;  }

注意,只要代码的执⾏时间不随 n 的增大而增长,这样代码的时间复杂度都记作O(1)。或者说,⼀般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。

O(n)分析法

O(n)表示线性时间复杂度,也就是说,算法的速度与输入数据的数量呈线性关系。

代码示例:

#include <stdio.h>  // 计算一个整数数组中所有元素的和int sum_array(int arr[], int n) {      int sum = 0;      for (int i = 0; i < n; i++)     {          sum += arr[i];     	}      	return sum;  }  int main() {      int arr[] = {1, 2, 3, 4, 5};      int n = sizeof(arr) / sizeof(arr[0]);      printf("Sum of array elements is %d\n", sum_array(arr, n));          return 0;  }
O(log n)分析法

O(log n)表示对数时间复杂度。当输入数据的数量每扩大一定倍数(如两倍),算法的执行时间只增加常数。

代码示例:

// 假设有一个已排序的数组和一个目标值,// 二分查找的伪代码  int binary_search(int arr[], int n, int target) {      int left = 0, right = n - 1;      while (left <= right)     {          int mid = left + (right - left) / 2;          if (arr[mid] == target)         {              return mid;          }         else if (arr[mid] < target)         {              left = mid + 1;          }         else {              right = mid - 1;          }      }      return -1; // 没找到  }
O(n^2)分析法

O(n^2)表示平方时间复杂度。它表示算法的执行时间与输入数据数量的平方成正比。

代码示例:

#include <stdio.h>  // 冒泡排序void bubble_sort(int arr[], int n) {      for (int i = 0; i < n - 1; i++)     {         for (int j = 0; j < n - i - 1; j++)        {             if (arr[j] > arr[j + 1])            {                 // 交换 arr[j] 和 arr[j + 1]                 int temp = arr[j];                 arr[j] = arr[j + 1];                 arr[j + 1] = temp;             }         }     }  }  int main() {      int arr[] = {64, 34, 25, 12, 22, 11, 90};      int n = sizeof(arr) / sizeof(arr[0]);      bubble_sort(arr, n);          printf("Sorted array: \n");      for (int i = 0; i < n; i++) {          printf("%d ", arr[i]);      }          return 0;  }
O(n *log n)分析法

O(n * log n)表示算法的执行时间同时与输入数据的数量和其对数成正比。

代码示例:

#include <stdio.h>  #include <stdlib.h>    // 合并两个已排序的数组  void merge(int arr[], int left[], int lsize, int right[], int rsize, int merged[], int msize) {      int i = 0, j = 0, k = 0;      while (i < lsize && j < rsize)     {          if (left[i] <= right[j])         {              merged[k++] = left[i++];          }         else {              merged[k++] = right[j++];          }      }        while (i < lsize)     {          merged[k++] = left[i++];      }      while (j < rsize)     {          merged[k++] = right[j++];      }  }    // 归并排序函数  void merge_sort(int arr[], int size) {      if (size <= 1)     {          return; // 无需排序      }          int mid = size / 2;      int *left = (int *)malloc(mid * sizeof(int));      int *right = (int *)malloc((size - mid) * sizeof(int));      for (int i = 0; i < mid; i++)     {          left[i] = arr[i];      }      for (int i = mid; i < size; i++)     {          right[i - mid] = arr[i];      }        merge_sort(left, mid);      merge_sort(right, size - mid);      merge(arr, left, mid, right, size - mid, arr, size);        free(left);      free(right);  }    int main() {      int arr[] = {64, 34, 25, 12, 22, 11, 90};      int size = sizeof(arr) / sizeof(arr[0]);            printf("Original array: \n");      for (int i = 0; i < size; i++)     {          printf("%d ", arr[i]);      }            merge_sort(arr, size);            printf("\nSorted array: \n");      for (int i = 0; i < size; i++)     {          printf("%d ", arr[i]);      }            return 0;  }
O(n!)分析法

O(n!)表示阶乘时间复杂度。当输入数据的数量增加时,算法的执行时间以阶乘的方式增长。

代码示例:

#include <stdio.h>  #include <stdlib.h>    void swap(int *num1, int *num2) {      int temp = *num1;      *num1 = *num2;      *num2 = temp;  }  // 排列算法void permute(int *arr, int start, int end) {      if (start == end)    {          for (int i = 0; i <= end; i++)         {              printf("%d ", arr[i]);          }          printf("\n");      }     else     {          for (int i = start; i <= end; i++)         {              swap((arr + start), (arr + i));              permute(arr, start + 1, end);              swap((arr + start), (arr + i));        }      }  }    int main(){      int arr[] = {1, 2, 3};      int n = sizeof(arr) / sizeof(arr[0]);      permute(arr, 0, n - 1);          return 0;  }
算法时间度对比

从图中可以看出,时间复杂从下到上复杂度越来越大(O(1)-->O(logn)-->O(n)-->O(nlogn)-->O(n^2)-->O(2^n) --> O(n!)),也意味着执行效率越来越低。其中,

O(1):常数时间复杂度,表示算法执行所需的时间不随输入数据规模n的变化而变化。O(logn):对数时间复杂度,通常出现在采用分治策略的算法。O(n):线性时间复杂度,表示算法执行时间随输入数据规模n线性增长。O(nlogn):线性对数时间复杂度,是许多高效排序算法的时间复杂度。这些算法通过优化分治策略,在保持对数级别深度递归的同时,每个递归步骤处理线性数量的数据。O(n^2):平方和立方时间复杂度,通常出现在涉及嵌套循环的算法中法。随着n的增长,这些算法的执行时间迅速增加。O(2^n) 和 O(n!):指数和阶乘时间复杂度,这两种复杂度通常出现在需要遍历所有可能组合或排列的算法中。由于它们的增长速度非常快,这些算法在处理大规模数据时往往不切实际。

注意,

相同的时间复杂度并不意味着算法性能相同。算法的实际性能还受到其他因素的影响,如空间复杂度、数据分布、硬件性能等。对于同一个问题,可能存在多个具有不同时间复杂度的算法。在选择算法时,需要根据实际需求和约束条件进行权衡。在评估算法性能时,除了考虑时间复杂度外,还需要关注算法的空间复杂度、稳定性和健壮性等方面。算法空间复杂度空间复杂度概述

算法空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。其中,存储空间包括:

固定空间:这部分空间与输入数据的大小无关,是算法在执行过程中始终需要的空间,例如用于存储固定数量的变量或常量的空间。可变空间:这部分空间的大小随输入数据的规模而变化。它通常包括动态分配的内存空间,如递归调用栈空间、动态数组或链表等数据结构占用的空间。输入数据空间:这通常不计算在空间复杂度中,因为输入数据本身的空间是算法无法控制的。然而,在某些情况下,如果算法需要对输入数据进行复制或创建其副本,那么这些复制的数据可能会占用额外的空间。

算法空间复杂度也是采用大O表示法,大O表示法用于描述当输入规模 n 趋近于无穷大时,算法所需空间的上限增长速度。具体表达式如下:

S = O(f(n))

其中,

S 代表空间复杂度f(n) 是一个关于输入规模 n 的函数空间复杂度常用表示方法

空间复杂度的表示方法可以细分为以下几种情况:

常量空间:当存储空间大小固定,与输入数据规模n无关时,空间复杂度为O(1)。这表示无论输入数据如何变化,算法所需的额外空间始终保持不变。例如,

// 数据交换void swap(int* num1, int* num2) {      int temp = *num1;      *num1 = *num2;      *num2 = temp;  }
线性空间:如果算法中定义了一个线性集合(如列表),并且该集合的大小与输入规模n成正比,则空间复杂度为O(n)。这表示算法所需的额外空间随着输入数据的增长而线性增长。例如,
// 计算一组数据的平局值double average(int* nums, int n) {      int sum = 0;      for (int i = 0; i < n; i++)     {          sum += nums[i];      }      return (double)sum / n;  }
二维空间:当算法中定义了一个二维列表集合,且集合的长和宽都与输入规模n成正比时,空间复杂度可以表示为O(n^2)或O(nm),其中m是另一个与输入规模相关的参数。例如,
// 矩阵运算void transposeMatrix(int** matrix, int n, int m) {      int** transposed = (int**)malloc(m * sizeof(int*));      for (int i = 0; i < m; i++)     {          transposed[i] = (int*)malloc(n * sizeof(int));      }        for (int i = 0; i < n; i++)     {          for (int j = 0; j < m; j++)         {              transposed[j][i] = matrix[i][j];          }      }      // 释放transposed矩阵占用的内存      for (int i = 0; i < m; i++)     {          free(transposed[i]);      }          free(transposed);  }
递归空间:对于递归算法,其空间复杂度通常与递归的深度有关。递归过程涉及进栈和出栈操作,每次递归调用都会将相关信息压入栈中。如果递归的深度是n,那么空间复杂度就是O(n)。
// 计算斐波那契数列int fibonacci(int n) {      if (n <= 1) 	{          return n;      }     else     {          return fibonacci(n - 1) + fibonacci(n - 2);      }  }

标签: #算法的重要特性是指