时间复杂度
常数时间的操作:一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。譬如数组寻址或者加减乘除运算等操作。
时间复杂度:在一个算法流程中,常数操作数量的指标,常用O(big O)来表示。具体来说,在常数操作数量的表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果记为f(N),那么时间复杂度为O(f(N))。
注意:评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项的时间。
详细的注解:如何理解算法时间的复杂度表示法
冒泡排序
冒泡排序:按照顺序比较相邻的数并交换位置(如果不是预期的顺序)的一种算法。顺序可以是升序或者降序。冒泡排序的本质就是从一个元素开始,不断比较相邻位置的数,如果不是预期的顺序就交换,直到交换到最后一个位置(这时候我们已经把最大的数放到了最后的位置)。然后我们进行N-1次循环,每次仅排列好一个数,我们就能得到一个排好序的数组。每经过一次循环排列好的数就多一个,所以需要比较的数的数量在每次循环中也相应的减少。
详细的注解:冒泡排序
# Bubble sort in Python
def bubbleSort(array):
for i in range(len(array)):
for j in range(0, len(array)-i-1):
if array[j] > array[j+1]:
(array[j], array[j+1]) = (array[j+1], array[j])
时间复杂度:O(N2)
选择排序
选择排序:在每次循环中记录最小位置的下标,然后交换到相应最小的位置。
详细的注解:选择排序
# Selection sort in Python
def selectionSort(array):
for step in rage(len(array)):
min_idx = step
for i in range(step+1, len(array)):
if array[i] < array[min_idx]:
min_idx = i
(array[step], array[min_idx]) = (array[min_idx], array[step])
时间复杂度:O(N2)
注意: 冒泡排序和选择排序在工程的实践中已经基本不用了。
插入排序
插入排序:插入排序是在每次循环中排列好一个区间范围内的数。在具体排列的时候,第一次循环的区间范围为1,所以它已经是排列好的。第二次循环的区间范围为2,然后将新加入的数依次向前比较,将其插入到合适的位置。之后依次循环,直至区间范围为整个数组。
详细的注解:插入排序
# Insertion sort in Python
def insertionSort(array):
for step in range(1, len(array)):
key = array[step]:
j = step - 1
while j>=0 and key < array[j]:
array[j+1] = array[j]
j = j - 1
array[j+1] = key
时间复杂度:
- 最坏情况:O(N2)
- 最好情况:O(N)
冒泡排序和选择排序都是严格的O(N2)算法,和数据的状况无关。但是插入排序是和数据状况相关的。
归并排序
归并排序是分治算法(divide and conquer)的一种。分治算法我会再另写一个博客来描述常见的分治问题。归并排序其实主要分为两步:第一步是将问题划分为子问题,第二部是通过试用类似于外排的方式来讲子数组排列好。
归并排序的分治策略:
假设我们需要排列一个无序数组A。子问题会变成排列这个数组的一部分:索引从p开始,从r结束,表示为A[p…r]。
- Divide:如果q是p…r的中点,那么我们把数组A给划分成两部分:A[p…q]和A[q+1, r]。
- Conquer:我们分别排列A[p…q]和A[q+1, r]这两部分。如果我们没有达到基线条件(base condition),我们继续划分子问题直到他们满足基线条件。
- Combine:在conquer那一步中我们最终会得到两个排列好的子数组A[p…q]和A[q+1, r],然后我们组合这两个结果然后得到最红的有序数组A[p…r]。(使用类似外排的方式来merge/ combine)
详细的注解:归并排序
# Merge sort in Python
def mergeSort(array):
if len(array) > 1:
mid = len(array) // 2
L = array[:mid] # Dividing the array into 2 parts
R = array[mid:]
# Recursion is used here
mergeSort(L)
mergeSort(R)
# The following is the process of merging
i = j = k = 0
# Copy data to temp arrays L[] and R[]
while i < len(L) and j < len(R):
if L[i] < R[j]:
arr[k] = L[i]
i += 1
else:
arr[k] = R[j]
j += 1
k += 1
# Checking if any element was left
while i < len(L):
arr[k] = L(i)
i += 1
k += 1
while j < len(R):
arr[k] = R[j]
j += 1
k += 1
时间复杂度:O(N*logN)
归并排序算法思路应用
小和问题
在一个数组中,每一个元素左边比当前元素值小的元素值累加起来,叫做这个数组的小和。
例如:[2,3,4,1,5]
2左边比2小的元素:无
3左边比3小的元素:2
4左边比4小的元素:2,3
1左边比1小的元素:无
5左边比5小的元素:2,3,4,1
小和small_sum = 2 + 2 + 3 + 2 + 3 + 4 + 1 = 17
思路:要计算一组数的小和,只需要计算左边的小和加上右边的小和再加上合并时候的小和。合并时候的小和求解的时候已经排好序了,计算小和时可以用这一策略进行加速。
def small_sub(arr):
return merge_sort(arr, 0, len(arr)-1)
def merge_sort(arr, left, right):
if left == right:
return 0
mid = (left + right) // 2
return merge_sort(arr, left, mid) + merge_sort(arr, mid+1, right) + merge(arr, left, mid, right)
def merge(arr, left, mid, right):
res = [] # 辅助数组
small_sub = 0
left_index = left
right_index = mid + 1
while (left_index<=mid and right_index<=right):
if arr[left_index] <= arr[right_index]:
small_sub += arr[left_index] * (right - right_index + 1)
res.append(arr[left_index])
left_index += 1
else:
res.append(arr[right_index])
right_index += 1
res += arr[left_index:mid+1]
res += arr[right_index:right+1]
for i in range(len(res)):
arr[left+i] = res[i] # 将原数组变得有序
return small_sub
if __name__ == '__main__':
x = [1,3,4,2,5]
res = small_sub(x)
print(res)
逆序对儿问题
于一个序列a1, a2, a3…an, 若存在i, j,使得i<j且ai>aj,则称为一个逆序对儿。输入为一个数组,输出为数组中逆序对儿数目。
class inverseCount:
def __init__(self):
self.count = 0
def mergeSort(self, array):
if len(array) < 2:
return array
mid = len(array) // 2
left = self.mergeSort(data[:mid])
right = self.mergeSort(data[mid:])
i, j = 0
res = []
while (i < len(left) and j < len(right)):
if left[i] <= right[j]:
res.append(left[i])
i += 1
else:
res.append(right[j])
j += 1
self.count += (len(left)-i)
return res + left[i:] + right[j:]
def InversePairs(self, array):
if len(data) < 2:
return 0
selg.mergeSort(data)
return self.count
对数器
对数器是通过用大量测试数据来验证算法是否正确的一种方式。
步骤:
- 有一个你想要测的方法a;
- 实现一个绝对正确但是复杂度不好的b;
- 实现一个随机样本产生器;
- 实现对比算法a和b的方法;
- 把方法a和方法b比对多次来验证方法a是否正确;
- 如果有一个样本使得比对出错,打印样本分析是哪个方法出错;
- 当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
注意:
- 随机产生的样本应该是小数据集,但是要进行多次(10w+)的对比。小数据集是因为方便对比分析,多次比对是要覆盖所有的随机情况。
- 算法b要保持正确性。
主定理分析(master theorem)
主定理分析可以用来分析递归算法的时间复杂度。套公式就能得出来算法复杂度。注意n/b中b代表着子过程中的样本量,a代表子过程中发生了几次,f(n)代表除去调用子过程之外,剩下的代价是多少。(只有当子过程的样本量规模一样时,才可以调用下面的公式)