一、递归
概念:
函数直接或者间接的调用自身算法的过程,则该函数称为递归函数。在计算机编写程序中,递归算法对解决一大类问题是十分有效的。
特点:
①递归就是在过程或者函数里调用自身。
②在使用递归策略时,必须有一个明显的结束条件,称为递归出口。问题规模相比上次递归有所减少,
③递归算法解题通常显得很简洁,但递归算法解题的效率较低。所以一般不倡导使用递归算法设计程序。
④在递归调用的过程当中系统的每一层的返回点、局部变量等开辟了栈来存储。递归函数次数过多容易造成栈溢出等。
所以一般不倡导用递归算法设计程序。
要求:
递归算法所体现的"重复"一般有三个条件:
①每次在调用规模上都有所缩小(通常是减半)。
②相邻两次重复之间有紧密的联系,前一次要为后一次做准备(通常前一次的输出就作为后一次的输入)。
③在问题的规模极小时必须用直接解答而不再进行递归调用,因而每次递归调用都是有条件的(以规模未达到直接解答的大小为条件),
无条件的递归调用将会成为死循环而不能正常结束。
分析以下函数的执行过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
def func3(x): if x> 0 : print (x) func3(x - 1 ) func3( 5 ) def func4(x): if x> 0 : func4(x - 1 ) print (x) func4( 5 ) |
根据Python执行的过程,及函数调用去分析执行结果!
关于斐波拉契数列
斐波拉契数列指的是这样一个数列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368。
特别注意:第0项是0,第1项是第一个1。这个数列从第2项开始,每一项都等于前两项之和。
def fibo(n): before = 0 after = 1 if n == 0 or n == 1: return n if n <= 3: return 1 return fibo(n-1)+fibo(n-2) print(fibo(3))
二分查找
从有序列表的候选区data[0:n]开始,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半。
特点:
二分查找算法就是不断将数组进行对半分割,每次拿中间元素和要找的元素进行比较。小就向右找,大就向左找!
要求:
在一段数字内,找到中间值,判断要找的值和中间值大小的比较。
如果中间值大一些,则在中间值的左侧区域继续按照上述方式查找。
如果中间值小一些,则在中间值的右侧区域继续按照上述方式查找。
直到找到我们希望的数字。
def search_data(data,data_find): # 中间值的索引号的定义:数组长度/2 mid = int(len(data)/2) # 判断从1开始的数字数组内查找 if data[mid] >= 1: # 如果我们要找的值(data_find)比中间值(data[mid])小 if data[mid] > data_find: print("你要找的数字比中间值[%s]小..." % data[mid]) # 在中间值(data[mid])的左侧继续查找,在此函数中继续循环 search_data(data[:mid],data_find) # 如果我们要找的值(data_find)比中间值(data[mid])大 elif data[mid] < data_find: print("你要找的数字比中间值[%s]大..." % data[mid]) # 在中间值(data[mid])的右侧继续查找,在此函数中继续循环 search_data(data[mid:],data_find) else: # 如果我们要找的值(data_find)既不比中间值(data[mid])大,也不比中间值(data[mid])小,则就是它 print("这就是你要找的[%s]!" % data[mid]) else: print("不好意思,没有找到你要的值...") if __name__ == ‘__main__‘: # 创建一个1到6000万的连续数字数组 data = list(range(60000000)) # 调用函数找到95938的值 search_data(data,95938)
列表查找
列表查找:从列表中查找指定元素
输入:列表、待查找元素
输出:元素下标或未查找到元素
一般是有两种方法:
1、顺序查找
从列表第一个元素开始,顺序进行搜索,直到找到为止。
1
2
3
4
|
def linear_search(data_set,value): for i in data_set: if data_set[i] = = value: return i |
2、二分查找
从有序列表的候选区data[0:n]开始,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半。
1
2
3
4
5
6
7
8
9
10
11
|
def bin_search(data_set,value): low = 0 high = len (data_set) - 1 while low < = high: mid = (low + high) / / 2 if data_set[mid] = = value: return mid elif data_set[mid] > value: high = mid - 1 else : low = mid + 1 |
def bin_search(data_set,value,low,high): if low <= high: mid = (low+high)//2 if data_set[mid] == value: return mid elif data_set[mid] >value: return bin_search(data_set,value,low,high) else: return bin_search(data_set, value, low, high) else: return
练习题:
1
2
3
4
5
6
7
8
9
|
现有一个学员信息列表(按 id 增序排列),格式为: [ { "id" : 1001 , "name" : "张三" , "age" : 20 }, { "id" : 1002 , "name" : "李四" , "age" : 25 }, { "id" : 1004 , "name" : "王五" , "age" : 23 }, { "id" : 1007 , "name" : "赵六" , "age" : 33 } ] 修改二分查找代码,输入学生 id ,输出该学生在列表中的下标,并输出完整学生信息。 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
l = [ { "id" : 1001 , "name" : "张三" , "age" : 20 }, { "id" : 1002 , "name" : "李四" , "age" : 25 }, { "id" : 1004 , "name" : "王五" , "age" : 23 }, { "id" : 1007 , "name" : "赵六" , "age" : 33 } ] def bin_search(data_set,value): """ 二分查找 :param data_set: 列表 :param value: 要查的值 :return: """ low = 0 high = len (data_set) - 1 while low < = high: mid = (low + high) / / 2 if data_set[mid][ ‘id‘ ] = = value: return (mid,data_set[mid]) elif data_set[mid][ ‘id‘ ] > value: high = mid - 1 else : low = mid + 1 else : return ( 0 , None ) flog = True while flog: sid = input ( "请输入学号(退出:Q):" ).strip() if sid.isdigit(): if sid.upper() = = "Q" : flog = False else : sid = int (sid) mid,infos = bin_search(l,sid) if not infos: print ( "查无此人!!!" ) else : s = "学生学号:{id},姓名:{name},年龄:{age}" . format ( * * infos) print ( "该学生所在信息索引坐标:%s" % mid) print ( "该学生的所有信息:%s" % s) |
快速排序 quick sort
介绍:
快速排序(Quicksort)是对冒泡排序的一种改进。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来,且在大部分真实世界的数据,可以决定设计的选择,减少所需时间的二次方项之可能性。
原理:
通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列,最终完成整个数据排序的目的。 值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。
步骤:
1、从数列中挑出一个元素,称为 “基准”(pivot),
2、重新排序数列,将基准归位!所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
注:在待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的;若具有相同关键字的记录之间的相对次序发生改变,则称这种排序方法是不稳定的。要注意的是,排序算法的稳定性是针对所有输入实例而言的。即在所有可能的输入实例中,只要有一个实例使得算法不满足稳定性要求,则该排序算法就是不稳定的。
革新点:先从后扫描(比基准值)小的,再从前扫描(比基准值)大的!
快速排序动画演示:
算法实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
import random as rd import time import sys sys.setrecursionlimit( 100000 ) def cal_time(func): """ 装饰器打印执行时间 """ def wrapper( * args, * * kwargs): t1 = time.time() x = func( * args, * * kwargs) t2 = time.time() print ( "%s running time %s secs." % (func.__name__, t2 - t1)) return x return wrapper def partition(data, left, right): """ 区内数据排序处理 快速排序的核心代码。 其实就是将选取的tmp不断交换,将比它小的换到左边,将比它大的换到右边。 它自己也在交换中不断变换自己的位置,直到完成所有的交换为止。 但在函数调用的过程中,pivot_key的值始终不变。 :param low:左边界索引 :param high:右边界索引 :return:分完左右区后tmp所在位置的索引 """ tmp = data[left] while left < right: while left < right and data[right]> = tmp: right - = 1 data[left] = data[right] while left < right and data[left]< = tmp: left + = 1 data[right] = data[left] data[left] = tmp return left def _quick_sort(data, left, right): """ 递归调用 """ if left < right: mid = partition(data, left, right) _quick_sort(data, left, mid - 1 ) _quick_sort(data, mid + 1 , right) @cal_time def quick_sort(data): """ 调用入口 """ return _quick_sort(data, 0 , len (data) - 1 ) li = list ( range ( 100000 )) rd.shuffle(li) quick_sort(li) |
总结:
- 快速排序的时间性能取决于递归的深度。
- 当tmp恰好处于记录关键码的中间值时,大小两区的划分比较均衡,接近一个平衡二叉树,此时的时间复杂度为O(nlog(n))。
- 当原记录集合是一个正序或逆序的情况下,分区的结果就是一棵斜树,其深度为n-1,每一次执行大小分区,都要使用n-i次比较,其最终时间复杂度为O(n^2)。
- 在一般情况下,通过数学归纳法可证明,快速排序的时间复杂度为O(nlog(n))。
- 但是由于关键字的比较和交换是跳跃式的,因此,快速排序是一种不稳定排序。
- 同时由于采用的递归技术,该算法需要一定的辅助空间,其空间复杂度为O(logn)。
参考博客:基于python的七种经典排序算法 [经典排序算法][集锦] 经典排序算法及python实现
首先明确,算法的实质 是 列表排序。具体就是操作的列表,将无序列表变成有序列表!
一、排序的基本概念和分类
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。
排序的稳定性:
经过某种排序后,如果两个记录序号同等,且两者在原无序记录中的先后秩序依然保持不变,则称所使用的排序方法是稳定的,反之是不稳定的。
内排序和外排序
内排序:排序过程中,待排序的所有记录全部放在内存中
外排序:排序过程中,使用到了外部存储。
通常讨论的都是内排序。
影响内排序算法性能的三个因素:
- 时间复杂度:即时间性能,高效率的排序算法应该是具有尽可能少的关键字比较次数和记录的移动次数
- 空间复杂度:主要是执行算法所需要的辅助空间,越少越好。
- 算法复杂性。主要是指代码的复杂性。
根据排序过程中借助的主要操作,可把内排序分为:
- 插入排序
- 交换排序
- 选择排序
- 归并排序
按照算法复杂度可分为两类:
- 简单算法:包括冒泡排序、简单选择排序和直接插入排序
- 改进算法:包括希尔排序、堆排序、归并排序和快速排序
排序lowB三人组
为什么叫排序lowB三人组呢?因为冒泡排序,选择排序,插入排序 这三个经典算法时间复杂度都是O(n2)。空间复杂度为O(1)。
冒泡排序 Bubble sort
介绍:
冒泡排序(Bubble Sort,台湾译为:泡沫排序或气泡排序)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端。
步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
核心:
原理是临近的数字两两进行比较,如果反序则交换,直到没有反序记录为止。按照从小到大或者从大到小的顺序进行交换。
以从小到大排序为例:
冒泡排序动画演示
算法实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
‘‘‘ 冒泡排序 ‘‘‘ def Dubble_sort(a): """ 两数相比较,非相邻 """ for i in range ( len (a)): for j in range (i + 1 , len (a)): if a[i] > a[j]: a[i],a[j] = a[j],a[i] return a def bubble_sort(li): """ 两数相比较,相邻 """ for i in range ( len (li) - 1 ): for j in range ( len (li) - i - 1 ): if li[j] > li[j + 1 ]: li[j],li[j + 1 ] = li[j + 1 ],li[j] return li #冒泡排序-优化 #如果冒泡排序中执行一趟而没有交换,则列表已经是有序状态,可以直接结束算法。 def bubble_sort_1(li): for i in range ( len (li) - 1 ): exchange = False for j in range ( len (li) - i - 1 ): if li[j] > li[j + 1 ]: li[j], li[j + 1 ] = li[j + 1 ], li[j] exchange = True if not exchange: return return li |
选择排序 Selection sort
介绍:
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
另一种解释就是,直接从待排序数组里选择一个最小(或最大)的数字,每次都拿一个最小数字出来,顺序放入新数组,直到全部拿完。再简单点,对着一群数组说:你们谁最小出列,站到最前边;然后继续对剩余的无序数组说:你们谁最小出列,站到刚才那位的后边;再继续刚才的操作,一直到最后一个。现在数组有序了,从小到大
选择排序是不稳定的排序方法(比如序列[5, 5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面)。
通俗的说就是,对尚未完成排序的所有元素,从头到尾比一遍,记录下最小的那个元素的下标,也就是该元素的位置。再把该元素交换到当前遍历的最前面。其效率之处在于,每一轮中比较了很多次,但只交换一次。因此虽然它的时间复杂度也是O(n^2),但比冒泡算法还是要好一点。
思路:
一趟遍历记录最小的数,放到第一个位置;
再一趟遍历记录剩余列表中最小的数,继续放置;
假如,有一个无须序列A=[6,3,1,9,2,5,8,7,4],选择排序的过程应该如下:
第一趟:选择最小的元素,然后将其放置在数组的第一个位置A[0],将A[0]=6和A[2]=1进行交换,此时A=[1,3,6,9,2,5,8,7,4];
第二趟:由于A[0]位置上已经是最小的元素了,所以这次从A[1]开始,在剩下的序列里再选择一个最小的元素将其与A[1]进行交换。即这趟选择过程找到了最小元素A[4]=2,然后与A[1]=3进行交换,此时A=[1,2,6,9,3,5,8,7,4];
第三趟:由于A[0]、A[1]已经有序,所以在A[2]~A[8]里再选择一个最小元素与A[2]进行交换,然后将这个过程一直循环下去直到A里所有的元素都排好序为止。这就是选择排序的精髓。因此,我们很容易写出选择排序的核心代码部分,即选择的过程,就是不断的比较、交换的过程。
整个选择的过程如下图所示:
选择排序动画展示:
算法实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
‘‘‘ 选择排序 ‘‘‘ def select_sort(li): for i in range ( len (li) - 1 ): min_loc = i for j in range (i + 1 , len (li)): if li[j] < li[min_loc]: min_loc = j if min_loc ! = i: li[i], li[min_loc] = li[min_loc], li[i] return li |
插入排序 Insertion sort
介绍:
有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到一种新的排序方法——插入排序法。
插入排序的基本操作就是将一个数据插入到已经排好序的有序数组中,从而得到一个新的、个数加一的有序数组,算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。
插入算法把要排序的数组分成两部分:第一部分包含了这个数组的所有元素,但将最后一个元素除外(让数组多一个空间才有插入的位置),而第二部分就只包含这一个元素(即待插入元素)。在第一部分排序完成后,再将这个最后元素插入到已排好序的第一部分中。
总结如下:
0、可以形象的理解为整理扑克牌!假设你拿到手的一套顺序很乱的牌,当你梳理的时候就想当于是在做 插入排序 操作。
1、插入排序就是每一步都将一个待排数据按其大小插入到已经排序的数据中的适当位置,直到全部插入完毕。
2、把列表分为有序区和无序区两个部分。最初有序区只有一个元素。
3、每次从无序区选择一个元素,插入到有序区的位置,直到无序区变空。
插入排序动画演示:
算法实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
""" 插入排序 (一般都会选取第一个数为起始数,同时索引查找也不能越界) """ def insert_sort(li): """ 数据换来换取的方法 """ for i in range ( 1 , len (li)): tmp = li[i] j = i - 1 while j > = 0 and tmp < li[j]: li[j + 1 ] = li[j] j = j - 1 li[j + 1 ] = tmp return li def Insertion_sort(a): """ 按照索引插来插去的办法 """ for i in range ( 1 , len (a)): j = i while j> 0 and a[j - 1 ]>a[i]: j - = 1 a.insert(j,a[i]) a.pop(i + 1 ) return a |
该算法需要一个记录的辅助空间。最好情况下,当原始数据就是有序的时候,只需要一轮对比,不需要移动记录,此时时间复杂度为O(n)。然而,这基本是幻想。
一、算法:
算法是对特定问题求解步骤的一种描述,是独立存在的一种解决问题的方法和思想。它是指令的有限序列,其中每一条指令表示一个或多个操作;
此外,成为一个算法需要满足以下条件或特性:
(1)有穷性。一个算法必须总是在执行有穷步之后结束,且每一步都可在有穷时间内完成。
(2)确定性。算法中每一条指令必须有确切的含义读者理解时不会产生二义性。并且,在任何条件下,算法只有唯一的一条执行路径,即对于相同的输入只能得出相同的输出。
(3)可行性。一个算法是能行的,即算法中描述的操作都是可以通过已经实现的基本运算执行有限次来实现的。
(4)输入。零个或多个的输入。
(5)输出。一个或多个的输出。
二、算法设计的要求
通常设计一个“好”的算法应考虑达到以下目标:
(1)正确性。对于合法输入能够得到满足的结果;算法能够处理非法处理,并得到合理结果;算法对于边界数据和压力数据都能得到满足的结果。
(2)可读性。算法要方便阅读,理解和交流,只有自己能看得懂,其它人都看不懂,谈和好算法。
(3)健壮性。算法不应该产生莫名其妙的结果,一会儿正确,一会儿又是其它结果。
(4)高性价比,效率与低存储量需求。利用最少的时间和资源得到满足要求的结果,可以通过(时间复杂度和空间复杂度来判定)
同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。
计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。这是一个关于代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号【O】表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,它考察当输入值大小趋近无穷时的情况。
算法复杂度分为时间复杂度和空间复杂度。其作用:时间复杂度是指执行算法所需要的计算工作量;而空间复杂度是指执行这个算法所需要的内存空间。(算法的复杂性体现在运行该算法时的计算机所需资源的多少上,计算机资源最重要的是时间和空间(即寄存器)资源,因此复杂度分为时间和空间复杂度)。
1、时间复杂度
(1)时间频度
一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为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))为算法的渐进时间复杂度 ,简称时间复杂度。O是数量级的符号。
在各种不同算法中,若算法中语句执行次数为一个常数,则时间复杂度为O(1)。另外,在时间频度不相同时,时间复杂度有可能相同,如T(n)=n2+3n+4与T(n)=4n2+2n+1它们的频度不同,但时间复杂度相同,都为O(n2)。
(3)最坏时间复杂度和平均时间复杂度
最坏情况下的时间复杂度称最坏时间复杂度。一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,分析最坏的情况以估算算法指向时间的一个上界。这就保证了算法的运行时间不会比任何更长。
在最坏情况下的时间复杂度为T(n)=0(n),它表示对于任何输入实例,该算法的运行时间不可能大于0(n)。 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,算法的期望运行时间。
指数阶0(2n),显然,时间复杂度为指数阶0(2n)的算法效率极低,当n值稍大时就无法应用。
按数量级递增排列,常见的时间复杂度有:
常数阶 | 对数阶 | 线性阶 | 线性对数阶 | 平方阶 | 立方阶 | …… | K次方阶 | 指数阶 |
O(1) | O(log2 n ) | O(n) | O(nlog2 n) | O(n2 ) | O(n3 ) | O(nk ) | O(2n ) |
复杂度低 ---->---->---->---->---->---->---->---->---->---->---->---->----> 复杂度高
随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
时间复杂度的分析方法:
1、时间复杂度就是函数中基本操作所执行的次数
2、一般默认的是最坏时间复杂度,即分析最坏情况下所能执行的次数
3、忽略掉常数项
4、关注运行时间的增长趋势,关注函数式中增长最快的表达式,忽略系数
5、计算时间复杂度是估算随着n的增长函数执行次数的增长趋势
6、递归算法的时间复杂度为:递归总次数 * 每次递归中基本操作所执行的次数
计算方法
1.一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
分析:随着模块n的增大,算法执行的时间的增长率和 f(n) 的增长率成正比,所以 f(n) 越小,算法的时间复杂度越低,算法的效率越高。
2. 在计算时间复杂度的时候,先找出算法的基本操作,然后根据相应的各语句确定它的执行次数,再找出 T(n) 的同数量级(它的同数量级有以下:1,log2n,n,n log2n ,n的平方,n的三次方,2的n次方,n!),找出后,f(n) = 该数量级,若 T(n)/f(n) 求极限可得到一常数c,则时间复杂度T(n) = O(f(n))
则有 T(n) = n 的平方+n的三次方,根据上面括号里的同数量级,我们可以确定 n的三次方 为T(n)的同数量级
则有 f(n) = n的三次方,然后根据 T(n)/f(n) 求极限可得到常数c
则该算法的时间复杂度:T(n) = O(n^3) 注:n^3即是n的3次方。
3.时间复杂度比较简单的计算方法是:看看有几重for循环,只有一重则时间复杂度为O(n),二重则为O(n^2),依此类推,如果有二分则为O(logn),二分例如快速幂、二分查找,如果一个for循环套一个二分,那么时间复杂度则为O(nlogn)。
空间复杂度:
算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数,与问题的规模没有关系。算法的空间复杂度S(n)定义为该算法所耗费空间的数量级。
S(n)=O(f(n)) 若算法执行时所需要的辅助空间相对于输入数据量n而言是一个常数,则称这个算法的辅助空间为O(1);
递归算法的空间复杂度:递归深度N*每次递归所要的辅助空间, 如果每次递归所需的辅助空间是常数,则递归的空间复杂度是 O(N).
空间复杂度的分析方法:
一个算法的空间复杂度S(n)定义为该算法所耗费的存储空间,它也是问题规模n的函数。空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。
一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。
一个算法的空间复杂度只考虑在运行过程中为局部变量分配的存储空间的大小,它包括为参数表中形参变量分配的存储空间和为在函数体中定义的局部变量分配的存储空间两个部分。
算法的空间复杂度一般也以数量级的形式给出。如当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为O(log2n);当一个算法的空间复杂度与n成线性比例关系时,可表示为O(n)。若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。
空间复杂度补充
一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分。
(1)固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
(2)可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。
一个算法所需的存储空间用f(n)表示。S(n)=O(f(n)) 其中n为问题的规模,S(n)表示空间复杂度。
时间与空间复杂度比较
对于一个算法,其时间复杂度和空间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间;反之,当追求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。
另外,算法的所有性能之间都存在着或多或少的相互影响。因此,当设计一个算法(特别是大型算法)时,要综合考虑算法的各项性能,算法的使用频率,算法处理的数据量的大小,算法描述语言的特性,算法运行的机器系统环境等各方面因素,才能够设计出比较好的算法。算法的时间复杂度和空间复杂度合称为算法的复杂度。
常用的算法的时间复杂度和空间复杂度:
排序法 | 平均时间 | 最差情形 | 稳定度 | 额外空间 | 备注 |
冒泡 | O(n2 ) | O(n2 ) | 稳定 | O(1) | n较小时较好 |
交换 | O(n2 ) | O(n2 ) | 不稳定 | O(1) | n较小时较好 |
选择 | O(n2 ) | O(n2 ) | 不稳定 | O(1) | n较小时较好 |
插入 | O(n2 ) | O(n2 ) | 稳定 | O(1) | 大部分已排序时较好 |
基数 | O(logR B) | O(logR B) | 稳定 | O(n) |
B是真数(0-9), R是基数(个十百) |
Shell | O(nlogn) | O(ns ) 1<s<2 | 不稳定 | O(1) |
s是所选分组 |
快速 | O(nlogn) | O(n2 ) | 不稳定 | O(nlogn) | n较大时较好 |
归并 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n较大时较好 |
堆 | O(nlogn) | O(nlogn) | 不稳定 | O(1) |
n较大时较好
|