一、数据结构-算法的时间复杂度和空间复杂度
在编程题之前,首先我们先来聊聊时间复杂度:
算法复杂度分为时间复杂度和空间复杂度。其作用: 时间复杂度是指执行算法所需要的计算工作量;而空间复杂度是指执行这个算法所需要的内存空间。(算法的复杂性体现在运行该算法时的计算机所需资源的多少上,计算机资源最重要的是时间和空间(即寄存器)资源,因此复杂度分为时间和空间复杂度)。
简单理解:
(1)时间复杂度:执行这个算法需要消耗多少时间。
时间复杂度:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。这是一个关于代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。
(2)空间复杂度:这个算法需要占用多少内存空间。
空间复杂度(Space Complexity) 是对一个算法在运行过程中临时占用存储空间大小的量度,记做 S(n)=O(f(n)) ,其中n为问题的规模。利用算法的空间复杂度,可以对算法的运行所需要的内存空间有个预先估计。
一个算法执行时除了需要存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些计算所需的辅助空间。算法执行时所需的存储空间包括以下两部分。
(1)固定部分。这部分空间的大小与输入/输出的数据的个数、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
(2)可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。
1.1 算法的时间复杂度
(1)语句频度T(n): 一个算法执行所花费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。而且一个算法花费的时间与算法中的基本操作语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度,记为T(n)。
(2)时间复杂度: 在刚才提到的语句频度中,n称为问题的规模,当n不断变化时,语句频度T(n)也会不断变化。但有时我们想知道它的变化呈现什么规律。为此,我们引入时间复杂度概念。 一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度。
T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+5n+6 与 T(n)=3n²+3n+2 它们的T(n) 不同,但时间复杂度相同,都为O(n²)。
(3)常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n),线性对数阶O(nlog2n),平方阶O(n2),立方阶O(n3), k次方阶O(nk),指数阶O(2n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
(4)平均时间复杂度和最坏时间复杂度:
平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
(5)如何求时间复杂度:
【1】如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。
public static void main(String[] args) { int x = 91; int y = 100; while (y > 0) { if (x > 100) { x = x - 10; y--; } else { x++; } } }123456789101112
该算法的时间复杂度为:O(1)
这个程序看起来有点吓人,总共循环运行了1100次,但是我们看到n没有?
没。这段程序的运行是和n无关的,
就算它再循环一万年,我们也不管他,只是一个常数阶的函数
【2】当有若干个循环语句时,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的频度f(n)决定的。
int x = 1; for (int i = 1; i <= n; i++) { for (int j = 1; j <= i; j++) { for (int k = 1; k <= j; k++) { x++; } } }12345678
该算法的时间复杂度为:O(n3)
该程序段中频度最大的语句是第5行的语句,内循环的执行次数虽然与问题规模n没有直接关系,但是却与外层循环的变量取值有关,而最外层循环的次数直接与n有关,因此该程序段的时间复杂度为 O(n3)
1 int i = n - 1;2 while (i >= 0 && (A[i] != k)) {3 i--;4 }5 return i;12345
该算法的时间复杂度为:O(n)
此算法中第3行语句的频度不仅与问题规模n有关,还与输入实例A中的各元素取值和k的取值有关:如果A中没有与k相等的元素,那么第3行语句的频度为 f(n)=n ,该程序段的时间复杂度为 O(n)
(6)用时间复杂度来评价算法的性能
用两个算法A1和A2求解同一问题,时间复杂度分别是O(100n2),O(5n3)
(1) 5n3/100n2=n/20 ,当输入量n<20时,100n2 > 5n3 ,这时A2花费的时间较少。
(2)随着问题规模n的增大,两个算法的时间开销之比 5n3/100n2=n/20 也随着增大。即当问题规模较大时,算法A1比算法A2要高效的多。它们的渐近时间复杂度O(n2)和O(n3) 评价了这两个算法在时间方面的性能。在算法分析时,往往对算法的时间复杂度和渐近时间复杂度不予区分,而经常是将渐近时间复杂度 O(f(n)) 简称为时间复杂度,其中的f(n)一般是算法中频度最大的语句频度。
(7)小结
算法的时间复杂度和两个因素有关:算法中的最大嵌套循环层数;最内层循环结构中循环的次数。
一般来说,具有多项式时间复杂度的算法是可以接受的;具有指数(不是对数)时间复杂度的算法,只有当n足够小时才可以使用。一般效率较好的算法要控制在O(log2n) 或者 O(n)
通常:容易计算的方法是看看有几重for循环,只有一重则时间复杂度为O(n),二重则为O(n^2),依此类推,如果有二分则为O(logn),二分例如快速幂、二分查找,如果一个for循环套一个二分,那么时间复杂度则为O(nlogn)
1.2常见算法时间复杂度整理
各种常用排序算法 | ||||||||
类别 | 排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 复杂性 | 特点 | ||
最好 | 平均 | 最坏 | 辅助存储 | 简单 | ||||
插入 排序 | 直接插入 | O(N) | O(N2) | O(N2) | O(1) | 稳定 | 简单 | |
希尔排序 | O(N) | O(N1.3) | O(N2) | O(1) | 不稳定 | 复杂 | ||
选择 排序 | 直接选择 | O(N) | O(N2) | O(N2) | O(1) | 不稳定 | ||
堆排序 | O(N*log2N) | O(N*log2N) | O(N*log2N) | O(1) | 不稳定 | 复杂 | ||
交换 排序 | 冒泡排序 | O(N) | O(N2) | O(N2) | O(1) | 稳定 | 简单 | 1、冒泡排序是一种用时间换空间的排序方法,n小时好 |
快速排序 | O(N*log2N) | O(N*log2N) | O(N2) | O(log2n)~O(n) | 不稳定 | 复杂 | 1、n大时好,快速排序比较占用内存,内存随n的增大而增大,但却是效率高不稳定的排序算法。 | |
归并排序 | O(N*log2N) | O(N*log2N) | O(N*log2N) | O(n) | 稳定 | 复杂 | 1、n大时好,归并比较占用内存,内存随n的增大而增大,但却是效率高且稳定的排序算法。 | |
基数排序 | O(d(r+n)) | O(d(r+n)) | O(d(r+n)) | O(rd+n) | 稳定 | 复杂 | ||
注:r代表关键字基数,d代表长度,n代表关键字个数 |
注:
1、归并排序每次递归都要用到一个辅助表,长度与待排序的表长度相同,虽然递归次数是O(log2n),但每次递归都会释放掉所占的辅助空间,
2、快速排序空间复杂度只是在通常情况下才为O(log2n),如果是最坏情况的话,很显然就要O(n)的空间了。当然,可以通过随机化选择pivot来将空间复杂度降低到O(log2n)。
1.3 算法的空间复杂度计算
举例分析算法的空间复杂度:
public void reserse(int[] a, int[] b) {
int n = a.length;
for (int i = 0; i < n; i++) {
b[i] = a[n - 1 - i];
}
}
参考前面的空间复杂度定义,上方的代码中,当程序调用 reserse() 方法时,要分配的内存空间包括:引用a、引用b、局部变量n、局部变量i
因此 f(n)=4 ,4为常量。所以该算法的空间复杂度 S(n)=O(1)
二、数组方面
注意*****以下程序在python3上进行执行1
1. 数组中重复的数字
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组【2,3,1,0,2,5,3】,那么对应的输出是第一个重复的数字2。
相似题型解答:统计一个数字在排序数组中出现的次数。
#官方答案# -*- coding:utf-8 -*-import collectionsclass Solution: # 这里要特别注意~找到任意重复的一个值并赋值到duplication # 函数返回True/False def duplicate(self, numbers, duplication): # write code here flag=False c=collections.Counter(numbers) for k,v in c.items(): if v>1: duplication=k flag=True break return flag,duplication12345678910111213141516
执行形式
numbers=[2,3,1,0,2,5,3]duplication=-1 Solution().duplicate(numbers,duplication)123
在python中也可以对numbera进行处理
b=[]for x in list(set(numbers)): b.append(numbers.count(x)) print(max(b))1234
2. 连续子数组的最大和
Kadane算法又被称为扫描法,为动态规划(dynamic programming)的一个典型应用。我们用DP来解决最大子数组和问题:对于数组a,用ci标记子数组a[0..i]的最大和,那么则有
ci=max{ai,ci−1+ai}ci=max{ai,ci−1+ai}
# -*- coding:utf-8 -*-#法一:动态规划 O(n),并输出了最大和元素所在的索引class Solution: def FindGreatestSumOfSubArray(self, array): if not array: return 0 start=[] res,cur=array[0],array[0] for i in range(1,len(array)): cur+=array[i] #每次对比计算和和原来数值的大小 res=max(res,cur) #判断新的累加和是否小于0,小于则赋予0 if cur<0: start=[] cur=0 else: #记录构成最大元素和的索引 start.append(i+1) return res,start1234567891011121314151617181920212223
测试
array=[1,2,-4,2,6,8] Solution().FindGreatestSumOfSubArray(array)12
法二:穷举法
i, j的for循环表示x[i..j],k的for循环用来计算x[i..j]之和。
maxsofar = 0for i = [0, n) for j = [i, n) sum = 0 for k = [i, j] sum += x[k] /* sum is sum of x[i..j] */ maxsofar = max(maxsofar, sum)12345678
有三层循环,穷举法的时间复杂度为O(n3)O(n3)
冒泡排序
主要是拿一个数与列表中所有的数进行比对,若比此数大(或者小),就交换位置
执行一次for循环,遍历第一个元素,放到最后的位置
l=[5,3,6,2,1,4,8,7,9]for j in range(len(l)-1): if l[j] > l[j+1]: l[j],l[j+1] = l[j+1],l[j]print(l)12345
执行2次循环,在第一次的基础之上,每次执行一个元素与其他元素进行比对,之后插入相应的末尾
l=[5,3,6,2,1,4,8,7,9]for i in range(len(l)-1): for j in range(len(l)-1-i): if l[j] > l[j+1]: l[j],l[j+1] = l[j+1],l[j]print(l)12345678
插入排序
有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序
基本思想为:每步将一个待排序的纪录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。
插入排序就是用一个数与一个已排好序的序列进行比对,从右向左进行。
例:5与3进行比较,5>3,将5与3不进行交换。l=[3,5],
此时再进行排序。4 < 5 l=[3,4,5] 3<4不进行交换,l=[3,4,5]
l=[1,5,4,7,9,3,2,6,8]#索引为0,注意从1开始for i in range(1,len(l)):#步长为-1 for j in range(i,0,-1): if l[j] < l[j-1]: l[j],l[j-1] = l[j-1],l[j] else: break123456789
二分法查找
二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。
此方法适用于不经常变动而查找频繁的有序列表。
首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
实例:
#encoding:utf-8l = [1, 2, 3, 4, 5, 6, 7, 8, 9] find_num = int(input('请输入一个数字:')) start = 0end = len(l) - 1while True: middle = (start + end) // 2 if find_num == l[middle]: print('找到了!索引是:', middle) break elif find_num > l[middle]: start = middle + 1 elif find_num < l[middle]: end = middle - 1 if start > end: print('没找到!', find_num) break123456789101112131415161718