常见算法设计方法-分治法

时间:2022-04-22
本文章向大家介绍常见算法设计方法-分治法,主要内容包括分治法(Devide & Conquer)、2. 举例分析、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

分治法(Devide & Conquer)

1. 常见步骤

  • Devide 把一个问题的特殊实例划分成若干个子问题
  • Conquer 递归地解决每个子问题
  • Combine 将每个子问题的答案组合成最终答案

2. 举例分析

归并排序就是常见的一种采用“分治法”进行设计的算法,以下先给出具体的C#版代码示例

    /// <summary>
    ///     对列表进行递归排序
    /// </summary>
    /// <param name="list">待排序数组</param>
    /// <returns></returns>
    public static List<int> Sort(List<int> list)
    {
        if (list.Count <= 1)
            return list;
        var mid = list.Count/2;
        var left = new List<int>(); 
        var right = new List<int>(); 
        
        // Devide
        for (var i = 0; i < mid; i++)
            left.Add(list[i]);
        for (var j = mid; j < list.Count; j++)
            right.Add(list[j]);
        // Conquer
        left = Sort(left);
        right = Sort(right);

        // Combine
        return Merge(left, right);
    }

    /// <summary>
    ///     合并已经排序好的两个List
    /// </summary>
    /// <param name="left">Left List</param>
    /// <param name="right">Right List</param>
    /// <returns></returns>
    private static List<int> Merge(List<int> left, List<int> right)
    {
        var temp = new List<int>();
        while ((left.Count > 0) && (right.Count > 0))
        {
            if (left[0] <= right[0])
            {
                temp.Add(left[0]);
                left.RemoveAt(0);
            }
            else
            {
                temp.Add(right[0]);
                right.RemoveAt(0);
            }
        }

        if (left.Count > 0)
        {
            foreach (int item in left)
            {
                temp.Add(item);
            }
        }
            
        if (right.Count > 0)
        {
            foreach (int item in right)
            {
                temp.Add(item);
            }
        }
        
        return temp;

    }

分析这个算法可以发现,归并算法的递归部分在于不断地将待排序数组分为左右两个等长的数组直至左右列表中都只含有一个元素,再继续进行Merge操作,前者递归所花费的时间可以简单表示成2T(n/2),后者排序可以认为是θ(n),则总时间可以表示成T(n)=2T(n/2)+θ(n)。

平均情况下,定义的T(n)=输入规模为n之下时所有可能输入的期望时间,θ是渐进符号一种,大家可以简单认为对于输入n,f(n)存在精确上下界

接下来在计算时间复杂度的时候,针对这个优雅的时间函数我们可以有两种解决办法,第一种是判断整个递归树的长度和叶节点的个数,第二种则是直接套用主定理公式进行分析。这里我们采用第二种主定理进行分析。

这里是对主定理的相关说明

针对T(n)=aT(n/b)+f(n)的函数式子(a≥1,b>1),我们可以知道归并排序算法的函数符合主定理的第二种情况,即如果存在常数k ≥ 0,有 f(n)=θ(n^(㏒{b}a((㏒n)^k)),则有T(n)=θ(n^(㏒{b}a((㏒n)^(k+1)))。这里的k=0,则归并算法最终的时间复杂度T(n)=θ(n㏒n)

额外补充一个二分法实例

    /// <summary>
    ///     二分法查找
    /// </summary>
    /// <param name="list">传入的有序列表</param>
    /// <param name="beginIndex">起始位置</param>
    /// <param name="endIndex">终止位置</param>
    /// <param name="x">需要查找的x</param>
    /// <returns>返回的列表索引</returns>
    public static int BinarySearch(List<int> list, int beginIndex, int endIndex, int x)
    {
        if ((x > list.LastOrDefault()) | (x < list.FirstOrDefault()))
            return -1;

        if (x == list[beginIndex])
            return beginIndex;

        if (x == list[endIndex])
            return endIndex;

        var mid = (beginIndex + endIndex)/2;
        if (x == list[mid])
            return mid;
        return x > list[mid] ? BinarySearch(list, mid, endIndex, x) : BinarySearch(list, beginIndex, mid, x);

    }