算法时间复杂度
常数时间的操作:一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作
时间复杂度为一个算法流程中,常数操作数量的指标。常用O(读作big O)来表示。具体来说,在常数操作数量的表达式中,
只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果记为f(N),那么时间复杂度为O(f(N))。
例如 O( 1/100 * N² + 100 N + 5 ) 我们也认为它的时间复杂度为 O(N²)读作big o
O (10000N) 时间复杂度为 O(N),并且指标上 O(N)> O(N²)
重点就是忽略系数和常数项
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项时间。下面是一个插入排序算法python实现
def insert_sort(collection):
# 插入排序,大放右小放左
# 若是小放左,小继续跟前一个比较
for i in range(len(collection)-1):
while i >= 0 and collection[i+1] < collection[i]:
collection[i], collection[i+1] = collection[i+1], collection[i]
# 神来之笔,把交换后的小的那个数再与前面比
# 值得注意的是,python的for不是通过++来下移,而是赋值!
i -= 1
return collection
这个插入排序假如要排序的序列一开始就是有序的,只要执行一遍for,也就是O(n)。最坏情况是逆序,此时时间复杂度就是O(n²)。所以此时时间复杂度取决于输入的数据,此时如何评价呢,一律用最差情况来估计时间复杂度!
# 归并排序 用到了递归
def merge_sort(collection):
# 先采用递归对把数组对半排列好
# 然后使用外排算法合并成一个有序数组
length = len(collection)
if length == 1:
return collection
mid = length // 2
left_part = merge_sort(collection[:mid])
right_part = merge_sort(collection[mid:])
i = 0
j = 0
k = 0
left_length = len(left_part)
right_length = len(right_part)
while i < left_length and j < right_length:
if left_part[i] < right_part[j] :
collection[k] = left_part[i]
i += 1
else:
collection[k] = right_part[j]
j += 1
k += 1
while i < left_length:
collection[k] = left_part[i]
i += 1
k += 1
while j < right_length:
collection[k] = right_part[j]
j += 1
k += 1
return collection
对于用到递归的时间复杂度都满足式子 T(N) = a*T(N/b) + O(N^d) ,比如上面的归并排序,T(N) = 2 * T(N/2) + O(N) ,第一部分是因为递归把数据各分分一半去排序,所以是 2 * T(N/2),第二部分外排最坏情况是N,所以是O(N)。那么这种式子如何计算时间复杂度,利用到了以下的式子。
T(N) = a*T(N/b) + O(Nd)
1) log(b,a) > d -> 复杂度为O(N^log(b,a))
2) log(b,a) = d -> 复杂度为O(N^d * logN)
3) log(b,a) < d -> 复杂度为O(N^d)
归并排序比冒泡和选择排序快的原因在于,选择和冒泡排序确定一个数的位置需要从头遍历一遍 ,其中进行了太多无效的比较,所以造成了时间的浪费。而归并排序快的原因,在于更好的利用了每次排序的信息,例如merge中两个有序数组的合并利用了外排。归并排序适合解决以下问题。
第一题代码答案,其实质是把数据[1,3,4,2,5]不断二分,最终分得左边两个数1,3的时候,很容易算出小和1,算完了小和之后就要对这一部分排序,即对这两个数排序(已经算了小和,排序对结果没有影响),当左边与右边比较的时候也是同样的道理,比如[1,4],与右边[ 2,5 ],利用外排的思想,首先lp指向1,rp指向2,比较可知2大于1,所以rp剩下所有部分都比1大会产生rl-j个1小和。基本就是这个思想,某个数产生的小和其实就是这个数乘以右边比它大的所有数的积。
while i < ll and j < rl:
if lp[i] < rp[j]:
answer += lp[i] * (rl - j) # 只是多了这一句
collection[k] = lp[i]
i += 1
else:
collection[k] = rp[j]
j += 1
k += 1
对数器
写完一道算法题,如何验证是否正确。首先要有随机数据生成器,第二是绝对正确的算法(时间复杂度可以差,但是绝对正确,也可以是内置函数等),最后就是大样本测试。(排序,堆都可以准备一个对数器再上场面试)
排序算法
快排
解决上面问题的思路:(荷兰国旗问题)初始化时,less下标为-1,代表没有数比num小,more下标为len(arry)+1,也是代表没有比num大的数。简单来说就是一开始less和more其实分别指向数组的第一个元素前面和最后一个元素的后面。然后从数组第一个开始遍历到最后一个。当cur小于num的时候,cur元素与less指向当前位置的后一个元素交换位置,然后less+1,即指向刚刚交换的元素,cur下移指向下一个数组元素。当cur等于num的时候,什么都不需要操作,直接cur下移。当cur大于num的时候,当前cur元素与more指向位置的前一个元素交换位置,然后more-1既前移指向刚刚交换的元素,关键点是此时cur不需要下移,而是需要再判断一次当前交换后的元素,这一点与cur小于num的情况截然不同,需要深入理解!。代码如下:
def solution(collection, num):
length = len(collection)
less = -1
more = length
i = 0
while i < length and i < more: # 不能忘记 i<more 这个条件!i碰到more代表已经完成
if collection[i] < num:
collection[less+1], collection[i] = collection[i], collection[less+1]
less += 1
elif collection[i] > num:
collection[more-1], collection[i] = collection[i], collection[more-1]
more -= 1
continue # 不需要i++,而是重新判断一次
i += 1
return collection
c = [8,6,4,3,7,9,2,5,6]
solution(c, 5.5)
print(c) # 打印 [5, 2, 4, 3, 9, 7, 6, 6, 8]
# pythonic方式 如下
def solution(collection, num):
less = [i for i in collection if i < num]
more = [i for i in collection if i > num]
same = [i for i in collection if i == num]
return less + same + more
快速排序就类似上面的衍生,我们可以观察到:当我们确定一个基准数时,左边放比它小的数,右边放比它大的数,那么一次流程下来,我们中间存放的就是基准数(一个或多个),左边是比基准数小的序列,右边是比基准数大的序列,也就是说我们确定了基准数在数据排序中绝对正确的位置。如果此时,我们采取递归的策略,分别从左边和右边序列中挑选一个基准数,一次次执行下去就会得到排好序的数组,这其实也是快排的思路。所谓经典快排就是基准数选择序列的最后一位(或者第一位)。但是我们可以举例看到,假如序列是[1,2,3,4,5],那么显然一开始取到5是基准数,没有右边序列,第二次取到4是基准数,那么整个时间复杂度是O(n²),所以要使得快排“快”,就应该尽量取到中间的基准数!(可以推导公式证明)因此,随机快排就是在数组中随机取基准数的快排方法,从概率上是比经典快排快的。以下是经典快排。
def quick_sort(collection, left, right):
if left >= right:
return
# 基准数
pivot = collection[right]
less = left - 1
more = right + 1
i = left
while i < more:
if collection[i] < pivot:
collection[i], collection[less+1] = collection[less+1], collection[i]
less += 1
elif collection[i] > pivot:
collection[i], collection[more - 1] = collection[more - 1], collection[i]
more -= 1
continue
i += 1
quick_sort(collection, left, less)
quick_sort(collection, more, right)
l = [3, 5, 7 ,9 ,2 ,7, 6]
quick_sort(l, 0, len(l)-1)
print(l) # 打印 [2, 3, 5, 6, 7, 7, 9]
# pythonic
def quick_sort_py(collection):
# 结束递归,而且是要返回list,不能是none
if len(collection) <= 1:
return collection
pivot = collection[-1]
less = [i for i in collection if i < pivot]
more = [i for i in collection if i > pivot]
same = [i for i in collection if i == pivot]
return quick_sort_py(less) + same + quick_sort_py(more)
荷兰国旗问题
leecode75 三色球排序 https://leetcode.com/problems/sort-colors/
class Solution:
def sortColors(self, nums) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
left = -1
right = len(nums)
index = 0
while index < right:
if nums[index] == 0:
nums[left+1], nums[index] = nums[index], nums[left+1]
left += 1
elif nums[index] == 2:
nums[right-1], nums[index] = nums[index], nums[right-1]
right -= 1
index -= 1 # 这一步还是关键,因为换过来的数还要再比较一次
index += 1
# return nums
堆排序
满二叉树与完全二叉树的概念。
用数组表示二叉树:
1.左,右子树表示:设当前节点的下标为i,则其左子树下标为2×i+1,右子树下标为2×i+2.
2.父节点表示:设当前节点的下标为i,其父节点下标为 (i-1)/2 。
大根堆与小根堆的概念。堆就是完全二叉树。
def heapify(unsort, index, heap_size):
# GitHub上 heapify功能一样,都是把值变小的某个节点重新向下调整
# 唯一不同的是GitHub上采用了递归,写起来简洁
left = index * 2 + 1
largest = left if left < heap_size and unsort[left] > unsort[index] else index
largest = left+1 if left+1 < heap_size and unsort[left+1] > unsort[largest] else largest
if largest == index:
return unsort
else:
unsort[largest], unsort[index] = unsort[index], unsort[largest]
heapify(unsort, largest, heap_size)
def heap_sort(unsort):
# GitHub上的堆排序只用到heapify而已,关键是生成最大堆也是用heapify
n = len(unsort)
# 利用heapify生成最大堆,从倒数第二层最右边开始,一个个从左遍历到顶进行heapify
for i in range(n//2-1, -1, -1): # -1代表遍历到0停止, -1代表倒序
heapify(unsort, i, n)
# 利用最大堆的特点,把顶元素跟最后元素交换,然后缩减heap_size,在heapify顶元素
for i in range(n-1, 0, -1):
unsort[0], unsort[i] = unsort[i], unsort[0]
heapify(unsort, 0, i)
return unsort
桶排序
桶排序并不是基于比较实现的排序算法,而是通过构建容器(桶)来容纳数据进行统计,最后再重构有序数组。其时间复杂度只有O(n)和额外空间复杂度O(N),适用于给定范围的少量数据,浪费一定空间。下面是一道经典题目,LeetCode164。题目明确要求不能适用桶排序,因此无法先排序再求值,但是可以借鉴桶排序的思想去coding。
def solution(collection):
# 桶排序的思想是构建容器进行分类,题目的关键点是相邻两数的差值
length = len(collection)
if length <= 1:
return 0 # 只有一个数,所以差值为0
# 构建容器,如一般的桶排序是构建length个容器,这里是length+1个,至少有一个容器是空的没有装任何值
# 保证了相邻差值最大的两数必定位于于不同容器之间,(为什么?这就是这个方法精妙之处!)
# 在这个前提下,下一个范围的最小值减去上一个范围的最大值必定存在最大相邻差值
all_max = max(collection)
all_min = min(collection)
if all_max == all_min:
return 0 # 最大值等于最小值,就没有差值了
# 列表初始化除了推导式之外还能用*
is_empty = [0]*(length+1) # 该桶有值为True
mins = [all_min]*(length+1) # 该桶对应范围内的最小值
maxs = [all_min]*(length+1) # 该桶对应范围内的最大值
bucket = lambda x: (x-all_min) * length//(all_max-all_min) # 传入某个元素,返回它所属桶的下标
for i in range(length):
bid = bucket(collection[i])
mins[bid] = min(mins[bid], collection[i]) if is_empty[bid] else collection[i]
maxs[bid] = max(maxs[bid], collection[i]) if is_empty[bid] else collection[i]
is_empty[bid] = 1
# 所有数据装入桶之后,相邻非空的桶之间最大值和最小值比较,得出最大差值
res = 0
last_max = maxs[0] # maxs[0]必定存在,因为就是最小值放的..
for i in range(1, length+1):
if is_empty[i]:
res = res if res > mins[i] - last_max else mins[i] - last_max
last_max = maxs[i]
return res
排序算法的稳定性概述
排序算法的稳定性是指未排序前的序列中,相同元素的相对位置在排序算法执行后并没有改变。具体应用:例如实现总分相同的同学中,根据数学成绩排序。我们可以先对数学成绩排序,再对总分进行排序,对总分进行排序时,稳定的排序算法可以做到保留数学成绩排序信息,这就是排序算法稳定性的意义。稳定的排序算法:冒泡排序,堆排序,插入排序,归并排序,不稳定的排序算法:随机快排(可以做到稳定,但是论文级别难度),选择排序