一、算法简介
线性时间:如果一个算法的时间复杂度为O(n),则称这个算法具有线性时间,或O(n)时间。
对数时间:若算法的T(n) = O(log n),则称其具有对数时间。
- 算法的速度指的并非时间,而是操作数的增速;
- 谈论算法的速度时,我们说的是随着输入的增加,其运行时间将以什么样的速度增加;
- 算法的运行时间用大O表示法表示;
- 大O表示法计算的是最糟糕情况的运行时间。
- 算法运行时间是从其增速的表示去度量的。
- 大O表示法在计算时n实际上为c*n,c指的是算法所需的固定时间量,这被称为常量,在两种算法的时间复杂度不同时,这种常量无关紧要,而当时间复杂度相同时,就需要考虑这个常量,这也是快速排序和合并排序虽然运行时间都为O(n*log n),但快速排序的速度比合并排序会快(因为快速排序遇到最糟情况的可能性比遇到平均情况的可能性小得多)。
旅行商(最短路径问题)问题的算法的运行时间为O(n!),当前计算机科学领域没有更有效率的算法。
二、选择排序
数组:
使用数组的优点:
每个元素在数组中的地址是被开发者所知的,所以读取时可直接按照索引进行读取(元素的位置称为索引)。
使用数组的缺点:
- 数据在内存中的储存地址是相连的,所以在声明的时候需要额外定义空地址以便保留新数据,若这些地址用不上,将会浪费内存。
- 储存数据超过数组容量时,需要重新定义。
链表:
链表的元素存储了下一个元素的地址,从而使一些不相连的内存地址串在了一起。也就是说只要有足够的内存空间就能为链表分配内存(优点)。
链表的缺点:
链表在读取元素时,由于不知道每个元素的地址,于是只能进行顺序访问,其时间复杂度为O(n);
名词解释:
索引:我们把元素的位置称为索引。
顺序访问:从第一个元素开始逐个地读取元素的访问方式。
随机访问:可直接通过索引进行读取的访问方式。
常见数组和链表操作的运行时间:
数组 |
链表 |
|
读取 |
O(1) |
O(n) |
插入 |
O(n) |
O(1) |
删除 |
O(n) |
O(1) |
注意:仅当能够立即访问要删除的元素时,删除操作的运行时间才为O(1);
三、递归
递归:
递归只是让解决方案更清晰,并没有性能上的优势。
实际上,在有些情况下,使用循环的性能更好。
Leigh Caldwell在Stack Overflow上说的一句话:“如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。如何选择要看什么对你来说更重要。”
每个递归函数都有两部分:基线条件(base case)和递归条件(recursive case)。
- 递归条件:指函数调用自己
- 基线条件:指函数不再调用自己,从而避免形成无限循环。
def countdown(i):
print(i)
if i <= 0: # 基线条件
return
else: # 递归条件
countdown(i-1)
# 调用:
countdown(4)
4
3
2
1
0
栈:
调用栈(call stack): 用于存储多个函数的变量。
- 调用另一个函数时,当前函数暂停并处于未完成状态。该函数的所有变量的值都还在内存中。执行完调用函数后,回到当前函数,并从离开的地方开始接着往下执行。
# 阶乘的递归 def fact(x): if x == 1: # 每个fact调用都有自己的x变量。在一个函数调用中不能访问另一个的x变量。 return 1 else: return x * fact(x-1)
用栈(递归调用栈)虽然很方便,但是也要付出代价:存储详尽的信息可能占用大量的内存。每个函数调用都要占用一定的内存,如果栈很高,就意味着计算机存储了大量函数调用的信息。在这种情况下,有两种选择:
- 重新编写代码,转而使用循环。
- 使用尾递归。但并非所有的语言都支持尾递归。
小结:
- 递归指的是调用自己的函数。
- 每个递归函数都有两个条件:基线条件和递归条件。
- 栈有两种操作:压入和弹出。
- 所有函数调用都进入调用栈。
- 调用栈可能很长,这将占用大量的内存。
四、快速排序
分治法(divide and conquer,D&C):
工作原理:
- 找出简单的基线条件;
- 确定如何缩小问题的规模,使其符合基线条件。
分治法的精髓:
分--将问题分解为规模更小的子问题;
治--将这些规模更小的子问题逐个击破;
合--将已解决的子问题合并,最终得出“母”问题的解;
编写涉及数组的递归函数时,基线条件通常是数组为空或只包含一个元素。
练习:
4.1 编写sum函数的代码:
def sum(arr):
if arr == []:
return 0
else:
return arr.pop()+sum(arr)
arr = [1,2,3,4]
summ = sum(arr)
print summ
标准答案:
def sum(list):
if list ==[]:
return 0
return list[0]+sum(list[1:])
4.2 编写一个递归函数来计算列表包含的元素数。
# -*- coding: utf8 -*-
def count(l) : #统计列表包含的元素数
if l==[]: #基线条件:列表包含元素为1
return 0
else:
del l[-1]
return 1 + count(l)
l =[1,2,3,4,5]
print count(l)
标准答案:
def count(list):
if list==[]:
return 0
return 1 + count(list[1:])
4.3 找出列表中最大的数字。
def findMaximum(arr): #使用的是循环方式
Maximum = arr[0]
for i in range(1,len(arr)):
if arr[i]>Maximum:
Maximum = arr[i]
return Maximum
arr = [1,5,8,2]
print findMaximum(arr)
标准答案:
def max(list):
if len(list) == 2: #基准条件
return list[0] if list[0] >list[1] else list[1]
sub_max = max(list[1:])
return list[0] if list[0] > sub_max else sub_max
print max([1,5,2,7])
4.4 二分查找法也是一种分治法,那么你能简述二分查找法的基线条件与递归条件吗?
基线条件:数组只包含一个元素。如果要查找的值与这个元素相同,就找到了,否则,就说明它不在数组中。
递归条件:当数组不止包含一个元素时,将数组分成两半,将其中一半丢弃,并对另一半执行二分查找。
快速排序:
步骤:
- 选择基准值。
- 将数组分成两个子数组:小于基准值的元素和大于基准值的元素。
- 对这两个子数组进行快速排序。
代码:
# -*- coding: UTF-8 -*-
def quicksort(array):
if len(array) < 2:
return array #基线条件:为空或只包含一个元素的数组是“有序”的
else:
pivot = array[0] #递归条件
less = [i for i in array[1:] if i<= pivot] #由所有小于基准值的元素组成的子数组
greater =[i for i in array[1:] if i> pivot] #由所有大于基准值的元素组成的子数组
return quicksort(less) + [pivot] + quicksort(greater)
print quicksort([10,5,2,3])
小结:
- 实现快速排序时,请随机地选择用作基准值的元素。快速排序的平均运行时间为O(n*log n)。
- 大O表示法中的常量有时候事关重大,这就是快速排序比合并排序快的原因所在。
- 比较简单查找和二分查找时,常量几乎无关紧要,因为列表很长时O(log n) 的速度比O(n)快得多。