感谢大家的支持和关注,大家一起进步
数据结构和算法评估
- 前言
- 算法效率的评估指标
- 先来几个简单的算法
- 它们的算法复杂度是多少呢
- 常见的数据结构
- 应用场景
- 小结
-
Tips:
重点关注标红部分
前言
评价一个算法的复杂度通常是通过分析它的时间复杂度和空间复杂度来进行的。
时间复杂度是指算法执行所需的时间量,它用大O符号来表示。时间复杂度可以分为最坏情况、平均情况和最好情况三种情况进行分析,一般情况下我们主要关注最坏情况下的时间复杂度。常见的时间复杂度有O(1)、O(log n)、O(n)、O(n log n)、O(n^2)等,复杂度越低表示算法执行速度越快。
空间复杂度是指算法执行所需的空间量。空间复杂度分为原地算法(O(1))和非原地算法(O(n))两种,原地算法是指算法执行过程中不需要额外的空间存储数据,非原地算法则需要额外的空间。
评价一个算法的复杂度需要考虑算法的输入规模、算法的步骤数、数据结构的使用等因素。同时还需要根据具体问题的需求和场景来综合考虑,比如对于某些实时性要求高的场景,可能需要更快的算法执行速度。
算法效率的评估指标
怎么快速的评估一个算法的复杂度,结合下面的步骤就可以快速的评估出来
-
时间复杂度
:
时间复杂度描述了算法执行所需时间随输入规模增长的趋势。它通常表示为一个大O符号(O(f(n))),其中n是输入规模(如数组长度、图节点数等),f(n)是一个关于n的函数,表示算法执行时间的上限。
常见的时间复杂度有O(1)(常数时间)、O(log n)(对数时间)、O(n)(线性时间)、O(n log n)(线性对数时间)、O(n2)(二次时间)、O(2n)(指数时间)等。
一般来说,时间复杂度越低,算法执行越快。因此,O(1)是最理想的复杂度,而O(2n)则通常被认为是不可接受的,因为当n增大时,算法执行时间将急剧增加。
空间复杂度:
空间复杂度
:
描述了算法执行过程中所需存储空间随输入规模增长的趋势。它也使用大O符号来表示。空间复杂度包括算法本身所需的存储空间(如变量、数组等)以及算法执行过程中产生的临时存储空间(如递归调用栈、辅助数组等)。
理想情况下,空间复杂度应该尽可能低,以减少内存使用和提高算法效率。
最坏情况、平均情况和最好情况
:算法的时间复杂度和空间复杂度通常是在最坏情况下分析的,但也可以考虑平均情况和最好情况。最坏情况给出了算法执行时间的上限,而平均情况和最好情况则提供了更全面的性能评估。
输入规模
:复杂度分析通常针对的是大输入规模的情况。对于小输入规模,即使算法复杂度较高,也可能在实际应用中表现出良好的性能。
常数因子和低阶项
:在大O符号表示中,常数因子和低阶项通常被忽略。然而,在实际应用中,它们可能对算法性能产生显著影响。因此,在比较两个算法时,除了考虑复杂度外,还需要考虑这些常数因子和低阶项。
实现细节和硬件环境
:算法的实际性能还受到实现细节(如编程语言、编译器优化等)和硬件环境(如CPU速度、内存大小等)的影响。因此,在评估算法性能时,需要综合考虑这些因素。
先来几个简单的算法
冒泡排序
(Bubble Sort)
冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复进行的,直到没有再需要交换的元素为止。
void bubbleSort(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]) {
// 交换元素
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
二分查找
(Binary Search)
二分查找是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。
int binarySearch(int arr[], int l, int r, int x) {
while (l <= r) {
int m = l + (r-l)/2;
if (arr[m] == x)
return m;
if (arr[m] < x)
l = m + 1;
else
r = m - 1;
}
return -1;
}
线性搜索
(Linear Search)
线性搜索是一种最简单的搜索算法,它从数组的第一个元素开始,一直比较到数组的最后一个元素,如果找到了就返回该元素的位置,否则返回-1表示未找到
int linearSearch(int arr[], int n, int x) {
for (int i = 0; i < n; i++) {
if (arr[i] == x)
return i;
}
return -1;
}
快速排序
(Quick Sort)
快速排序是一种分而治之的排序算法。它通过选择一个“基准”元素将数组分成两部分,其中一部分的所有元素都比基准元素小,另一部分的所有元素都比基准元素大,然后递归地对这两部分进行排序。
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
int partition(int arr[], int low, int high) {
int pivot = arr[high];
int i = (low - 1);
for (int j = low; j <= high - 1; j++) {
if (arr[j] < pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return (i + 1);
}
斐波那契数列
(Fibonacci Sequence)
斐波那契数列是一个著名的数列,其中每个数字(除了前两个)都是前两个数字的和。这个数列以递归的方式定义。
int fibonacci(int n) {
if (n <= 1)
return n;
return fibonacci(n-1) + fibonacci(n-2);
}
它们的算法复杂度是多少呢
- 常数阶O(1):表示算法的运行时间为常量,即不随输入规模的增大而改变。例如,访问数组中的某个元素、哈希表在理想情况下的查找操作等。
- 对数阶O(log2n):常见于
二分查找
算法。每次将查找范围缩小一半,因此时间复杂度为O(log2n)。 - 线性阶O(n):表示算法的运行时间与输入规模n成正比。例如,
线性查找、单链表遍历、栈和队列
的基本操作等。- 线性对数阶O(nlog2n):常见于一些排序算法,如归并排序、快速排序(平均情况)等。
- 平方阶O(n2):表示算法的运行时间与输入规模的平方成正比。例如,简单排序算法(如直接插入排序)、两个n阶矩阵的乘法运算等。
- 立方阶O(n3):表示算法的运行时间与输入规模的立方成正比。例如,三个n阶矩阵的乘法运算。
- k次方阶O(nk):表示算法的运行时间与输入规模的k次方成正比。随着k的增加,算法的执行效率会急剧下降。
- 指数阶O(2n):表示算法的运行时间与2的n次方成正比。这种算法的时间复杂度非常高,通常只适用于输入规模非常小的情况。例如,求具有n个元素集合的所有子集的算法。
关键词:折半,循环,循环嵌套次数,基于它们快速的判断
常见的数据结构
- 数组(Array):一组连续的内存空间,用于存储相同类型的数据。
- 链表(Linked List):由一系列节点组成,每个节点包含数据和指向下一个节点的指针。
- 栈(Stack):一种先进后出(LIFO)的数据结构,只能在栈顶进行操作。
- 队列(Queue):一种先进先出(FIFO)的数据结构,只能在队首和队尾进行操作。
- 树(Tree):由节点和边组成的层次结构,其中每个节点可以有零个或多个子节点。
- 图(Graph):由节点和边组成的非线性数据结构,用于表示网络或关系图。
- 哈希表(Hash Table):利用哈希函数实现的数据结构,用于快速查找和插入数据。
- 堆(Heap):用于实现优先队列的数据结构,具有特定的顺序性。
- 集合(Set):一种不允许重复元素的数据结构,用于存储无序的数据。
- 队列的双端队列(Deque):在队首和队尾都可以进行插入和删除操作的队列。
应用场景
数组
:
存储和访问一组元素
,比如存储学生的成绩;- 实现简单的
哈希表
,比如存储键值对; - 实现缓冲区,比如存储一定数量的最近的日志事件;
- 实现位图,比如标记一组元素的出现与否;
- 实现
堆
,比如存储优先级队列的元素。
链表
:
- 实现队列或栈,比如实现一个简单的任务调度器;
- 动态存储和访问数据,比如实现一个动态增长的字符串;
- 实现图的邻接表表示,比如表示社交网络的关系;
- 实现多级缓存,比如用链表存储最近使用的资源的页面。
3.
栈
:
- 实现函数调用栈,比如记录函数调用的顺序和返回地址;
- 解决括号匹配问题,比如检查表达式是否合法;
- 实现深度优先搜索算法,比如遍历图的所有节点;
- 实现逆波兰表达式计算器,比如计算简单的数学表达式;
- 实现
回溯算法
,比如解决八皇后问题。
队列
:
- 实现广度优先搜索算法,比如遍历图的所有节点;
- 实现
消息队列
,比如用于多个线程或进程之间的通信; - 实现任务队列,比如处理网络请求;
- 实现缓存淘汰算法,比如最近最少使用(LRU)算法;
- 实现
生产者-消费者模型
,比如多线程环境下的任务调度。
树
:
- 实现二叉搜索树,比如存储和查找关键字;
- 实现堆,比如实现优先级队列;
- 实现字典树,比如实现字符串匹配算法;
- 实现表达式树,比如计算数学表达式的值;
- 实现
文件系统
,比如用树结构表示目录和文件的层次关系。
小结
本编文章的主要目的是,怎么去评估一个算法的复杂度
,以及数据结构的应用场景
,以后你写代码可以想想,怎么写效率高,选择合适的数据结构存储数据。 相信你也一定有所收获,最后不要忘了去刷题: LeetCode。最后再次感谢大家的支持和关注
。