背景

虽然平时工作涉及的主要内容是NLP相关,但是在处理数据,以及实现相关NLP的过程中难免不遇上一些基本算法的内容。分治就是一种比较好的算法思维,有必要对他进行深入了解和熟练使用。

算法内容

所谓分治算法就是采取分而治之的思想。在分析一些问题的时候可以从以下三个步骤考虑能否使用:

  1. Divid: 待解决的问题能否分割成多个相同的子问题
  2. Conquer:将子问题找到合适的解决方法
  3. 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 ⌈log2​n⌉,在合并的时候需要进行比较,最后的时间复杂度就是 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 ⌈log2​n⌉(向上取整的结果),对应的算法复杂度就是 O ( log ⁡ 2 n ) O(\log_2n) O(log2​n)。如果数组是均匀分布的,还可以使用插值查找。

如果想在具体的题目上练习,可以在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
原理可参考如下动画:
请添加图片描述

更多推荐