目录
一、数据结构的概念
1.1. 什么是数据结构
1.2. 算法与数据结构的关系
二、算法效率
三、时间复杂度
3.1. 大O的渐进表⽰法
3.2. 计算冒泡排序的时间复杂度
3.3. 计算二分查找的时间复杂度
四、空间复杂度
4.1. 空间复杂度
4.2. 冒泡排序的空间复杂度
4.3. 斐波那契数列的空间复杂度
五、学习时间复杂度和空间复杂度的好处
一、数据结构的概念
1.1. 什么是数据结构
什么是数据结构呢?相信很多老铁尤其是非计算机专业的老铁还是第一次听说这个词。通俗地说,数据结构就是在内存当中对我们的数据进行一个管理和建立数据见关系,我们熟知的内存条(如下图所示)就是存储数据的介质,我们对数据的管理就是存储在内存条上的。
计算机这个学科最重要的是做软件开发,软件可以帮助我们实现各种功能,比如我们微信好友列表里面,表面上就是一堆姓名的数据,透过计算机我们看到的只是01010,而这些数据就存在内存条上。
1.2. 算法与数据结构的关系
算法就是定义良好的计算过程,它取⼀个或⼀组的值为输⼊,并产⽣出⼀个或⼀组值作 为输出。简单来说算法就是利用计算机处理问题的步骤,比如我们对数据进行一个排名,就要用到排序算法,以及需要这个区域来进行排名,就需要堆来实现。
那么数据结构与算法之间的关系呢?二者相辅相成,你中有我,我中有你。解决一些算法问题需要用到数据结构;实现一些数据结构时,又需要用到一些算法。
二、算法效率
如何衡量⼀个算法的好坏,就需要用到算法效率去衡量。算法效率分析分为两种:第⼀种是时间效率,第⼆种是空间效率。时间效率被称为时间复杂度,⽽空 间效率被称作空间复杂度。时间复杂度主要衡量的是⼀个算法的运⾏速度,⽽空间复杂度主要衡量⼀ 个算法所需要的额外空间。
通俗点说,时间复杂度和空间复杂度用来衡量代码消耗资源开销的多少,衡量的标准应该是与代码有关,与运行的设备无关。
三、时间复杂度
⼀个算法所花费的时间与其中语句的执⾏次数成正⽐例,算法中的 基本操作的执⾏次数,为算法的时间复杂度。站在数学的角度来看,时间复杂度可以看作是计算关键操作的次数与使用问题规模的函数关系,再对这个函数关系进行一个化简和近似。化简就是只取最高次项,把最高次项的系数也忽略掉,记作O() 。
下面博主将结合代码带大家来具体体会一下时间复杂度。
3.1. 大O的渐进表⽰法
import java.util.Scanner;public class Main { public static int func(int N){ int count = 0; for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { count++; } } for (int k = 0; k < 2*N; k++) { count++; } int M = 10; while((M--)>0){ count++; } return count; } public static void main(String[] args) { Scanner num = new Scanner(System.in); int b = num.nextInt(); int a = func(b); System.out.println(a); }}
上面的基本操作就是count的自增,那么count的被执行次数与N的函数关系就是。那么我们就要记作。
import java.util.Scanner;public class Main { public static int func(int N){ int count = 0; for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { count++; } }/* for (int k = 0; k < 2*N; k++) { count++; }*/ int M = 10; while((M--)>0){ count++; } return count; } public static void main(String[] args) { Scanner num = new Scanner(System.in); int b = num.nextInt(); int a = func(b); System.out.println(a); }}
上面这段代码的函数关系是,那么依旧是记作。随着N值的增大,两个函数的值会越来越接近。
所以说,计算时间复杂度,一定要先找到核心的基本操作是什么。这个基本操作可能是打印、修改、比较或者是删除。
我们下面一段代码,因为描述问题规模的方式,不一定就只有一个变量,也可以是多个。此时我们应该记作。
import java.util.Scanner;public class Main { public static int func2(int M,int N){ int count = 0; for (int k = 0; k < M; k++) { count++; } for (int k = 0; k < N; k++) { count++; } return count; } public static void main(String[] args) { Scanner num = new Scanner(System.in); int m = num.nextInt(); int n = num.nextInt(); System.out.println(func2(m, n)); }}
我们再来看下面一段代码,下面的时间复杂度就比较固定了,基本操作的次数与N无关,无论N是多少,count永远被循环了100次。那么此时的时间复杂度要记作,常数级时间复杂度。及时这里的i<1亿,时间复杂度照样是。
import java.util.Scanner;public class Main { public static int func3(int N){ int count = 0; for (int i = 0; i < 100; i++) { count++; } return count; } public static void main(String[] args) { Scanner num = new Scanner(System.in); int a = num.nextInt(); System.out.println(func3(a)); }}
如果说出现了两段代码,一个时间复杂度是,另一个是,那么不一定比慢,有可能当N比较小时,时间复杂度要比。所以说,在这里要牢记时间复杂度衡量的是问题规模和时间的变化趋势,不能直接决定快慢。
3.2. 计算冒泡排序的时间复杂度
public class Main { public static void BubbleSort(int[] arrays){ for (int end = arrays.length; end > 0; end--) { boolean sorted = true; for (int i = 0; i < end; i++) { if(arrays[i-1] > arrays[i]){ Swap(arrays, i - 1, i); sorted = false; } } if (sorted == true){ break; } } }}
我们想要计算这个BubbleSort的时间复杂度,同理,首先也要搞清楚基本操作是什么。其中我们调用Swap方法进行交换的时候,在里面是不涉及内嵌循环的,所以计算时间复杂度的时候不用计算Swap内部的交换。我们先看外层循环,起始end的值是数组的长度N;内层循环呢,由于end的值不固定,当end=N时,i循环了N次,当end=N-1时,i循环了N-1次。所以时间复杂度的计算就是,结果就是。
3.3. 计算二分查找的时间复杂度
public class Main { public static int BinarySearch(int[] arrays,int value){ int begin = 0; int end = arrays.length - 1; while(begin <= end){ int mid = begin + ((end-begin) / 2); if (arrays[mid] < value){ begin = mid + 1; }else if(arrays[mid] > value){ end = mid - 1; }else{ return mid; } } return -1; }}
二分查找,每循环一次,区间就会缩小一半,当区间缩小为1的时候,我们才得出“找到或者是没找到”的结论。我们可能说不好直接得出时间复杂度来,那我们就利用特殊值法。当数组元素为8时,最多要经历4次循环;当数组元素为16时,最多要经历5次循环。所以我们可以得出时间复杂度为。
对数级别的复杂度,及时问题规模变得很大,核心操作次数增长依然缓慢。
综合冒泡排序和二分查找的时间复杂度时,准确地说要涉及到三种情况:1.最好情况下,最需要循环一次就能找到;2.平均情况下,循环一半的次数;3.最坏情况下,循环了最多次。
四、空间复杂度
4.1. 空间复杂度
空间复杂度是衡量代码运行中消耗的临时空间,也就是我们的代码在运行时所创建的空间,至运行结束被销毁的空间。比如一个数组,长度为N,在后续代码中,并没有创建任何临时的空间。此时这个数组的空间复杂度就是常数级复杂度。
4.2. 冒泡排序的空间复杂度
public class Main { public static void BubbleSort(int[] arrays){ for (int end = arrays.length; end > 0; end--) { boolean sorted = true; for (int i = 0; i < end; i++) { if(arrays[i-1] > arrays[i]){ Swap(arrays, i - 1, i); sorted = false; } } if (sorted == true){ break; } } }}
上面的代码都创建了三个临时变量,sorted和i都创建了N次,那么空间复杂度就是2N+1,也就是。但其实不是的,因为空间和时间不一样,时间是一去不复返的,而空间是可以重复利用的,都是销毁了前一个才创建下一个,前一个和下一个共用同一块空间。一共三个临时变量,所以空间复杂度就是。
4.3. 斐波那契数列的空间复杂度
int[] fibonacci(int n) { long[] fibArray = new long[n + 1]; fibArray[0] = 0; fibArray[1] = 1; for (int i = 2; i <= n ; i++) { fibArray[i] = fibArray[i - 1] + fibArray [i - 2]; } return fibArray; }
我们可以看到这次创建了临时空间,方法进来之后才创建,方法出去之后才销毁。下面除了i,也没有创建额外的临时空间了,所以上面代码的空间复杂度才是。
五、学习时间复杂度和空间复杂度的好处
学习时间复杂度,目的是为了能够正确的使用数据结构。数据结构有很多种,比如数组、队列、哈希表、栈、二叉树以及平衡树等,这些数据结构在不同场景中会有不同的应用。要想知道那种数据结构更合适,就要使用时间复杂度和空间复杂度来衡量。