【算法与数据结构】分治(Divid Conquer)算法——以快排,归并排序,二分查找为例
背景
虽然平时工作涉及的主要内容是NLP相关,但是在处理数据,以及实现相关NLP的过程中难免不遇上一些基本算法的内容。分治就是一种比较好的算法思维,有必要对他进行深入了解和熟练使用。
算法内容
所谓分治算法就是采取分而治之的思想。在分析一些问题的时候可以从以下三个步骤考虑能否使用:
- Divid: 待解决的问题能否分割成多个相同的子问题
- Conquer:将子问题找到合适的解决方法
- Combine:将每个子问题的解决方法进行合并,形成主问题的最终解决方法
总结一下,这些问题的特征:子问题之间相互独立,子问题的解可以合并为原问题的解。例如在排序中,冒泡排序的时间复杂度 O ( n 2 ) O(n^2) O(n2),使用分治方法可以将时间复杂度做到 O ( n log n ) O(n\log n) O(nlogn)。
案例
归并排序(Merge Sort)案例
def merge_sort(data_list):
"""
归并排序
:param data_list: 待排序数组
:return: 排序后的结果
"""
if len(data_list) <= 1:
return data_list
def merge(_left: list, _right: list):
"""
排序后的结果合并
:param _left: 右边内容
:param _right: 左边内容
:return: 排序后的结果
"""
result = []
head_l = head_r = 0
while head_l < len(_left) and head_r < len(_right):
if _left[head_l] < _right[head_r]:
result.append(_left[head_l])
head_l += 1
else:
result.append(_right[head_r])
head_r += 1
if head_l < len(_left):
result.extend(_left[head_l:])
else:
result.extend(_right[head_r:])
return result
middle = len(data_list) // 2
left = merge_sort(data_list[: middle])
right = merge_sort(data_list[middle: ])
return merge(left, right)
if __name__ == '__main__':
result = merge_sort([0,3, 1, -1, 2, 4])
print(result) # [-1, 0, 1, 2, 3, 4]
对于归并排序的图解可参考:LeetCode上的介绍,感觉讲的挺好。时间复杂度方面:拆分的次数与下面二分查找一致为 ⌈ log 2 n ⌉ \lceil \log_2n \rceil ⌈log2n⌉,在合并的时候需要进行比较,最后的时间复杂度就是 n log n n\log_n nlogn.
二分查找(Binary Search)案例
def binary_search(data_list: list, target):
"""
二分查找(只返回列表中一个目标元素对应的索引)
:param data_list: 待查找的list,有序(升序、倒序皆可)的数字列表
:param target: 目标值
:return: 元素对应的索引,-1 表示没有找到
"""
start = 0
end = len(data_list)
while start <= end:
mid = int(start + (end - start) / 2) # 中间值索引
mid_value = data_list[mid]
if target == mid_value:
return mid
elif mid_value > target:
end = mid - 1
else:
start = mid + 1
return -1
if __name__ == '__main__':
result = binary_search([-1, 0, 1, 2, 3, 4], 0)
print(result) # 1
其中数据查找的次数为 ⌈ log 2 n ⌉ \lceil \log_2n \rceil ⌈log2n⌉(向上取整的结果),对应的算法复杂度就是 O ( log 2 n ) O(\log_2n) O(log2n)。如果数组是均匀分布的,还可以使用插值查找。
如果想在具体的题目上练习,可以在LeetCode上尝试一下:二分查找题目。
快速排序(Quick Sort)案例
从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
def quick_sort(data_list: list, left=None, right=None):
left = 0 if not isinstance(left, (int, float)) else left
right = len(data_list) - 1 if not isinstance(right, (int, float)) else right
def partition(arr, _left, _right):
"""
分区
:param arr: 待分区数组
:param _left: 起始index
:param _right: 结束index
:return: 基准index
"""
pivot = left # 从最左边开始
i = index = pivot + 1 # 最左边的下一个值开始比较
while i <= _right:
if arr[i] < arr[pivot]: # 比基准小的值放到基准的左边(先放到连续的右边)
arr[i], arr[index] = arr[index], arr[i]
index += 1 # 记录基准小的值的边界
i += 1
# 基准与边界值交换完成基准值左边比基准值小,右边比基准值大
arr[pivot], arr[index - 1] = arr[index - 1], arr[pivot]
return index - 1 # 返回基准值索引
if left < right:
partition_index = partition(data_list, left, right)
quick_sort(data_list, left, partition_index - 1)
quick_sort(data_list, partition_index + 1, right)
return data_list
对于快速排序的图解可参考LeetCode
原理可参考如下动画:

更多推荐


所有评论(0)