算法思想 - 分治算法

分治算法简介

分治(divide and conquer),全称分而治之,是一种非常重要且常见的算法策略。

分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。

分治通常基于递归实现,包括“分”和“治”两个步骤。

  1. 分(划分阶段):递归地将原问题分解为两个或多个子问题,直至到达最小子问题时终止。
  2. 治(合并阶段):从已知解的最小子问题开始,从底至顶地将子问题的解进行合并,从而构建出原问题的解。

如下图所示,“归并排序”是分治策略的典型应用之一。

  1. :递归地将原数组(原问题)划分为两个子数组(子问题),直到子数组只剩一个元素(最小子问题)。
  2. :从底至顶地将有序的子数组(子问题的解)进行合并,从而得到有序的原数组(原问题的解)。

何时使用分治

一个问题是否适合使用分治解决,通常可以参考以下几个判断依据。

  1. 问题可以分解:原问题可以分解成规模更小、类似的子问题,以及能够以相同方式递归地进行划分。
  2. 子问题是独立的:子问题之间没有重叠,互不依赖,可以独立解决。
  3. 子问题的解可以合并:原问题的解通过合并子问题的解得来。

显然,归并排序满足以上三个判断依据。

  1. 问题可以分解:递归地将数组(原问题)划分为两个子数组(子问题)。
  2. 子问题是独立的:每个子数组都可以独立地进行排序(子问题可以独立进行求解)。
  3. 子问题的解可以合并:两个有序子数组(子问题的解)可以合并为一个有序数组(原问题的解)。

好处:提升算法效率

分治不仅可以有效地解决算法问题,往往还可以提升算法效率。在排序算法中,快速排序、归并排序、堆排序相较于选择、冒泡、插入排序更快,就是因为它们应用了分治策略。

那么,我们不禁发问:为什么分治可以提升算法效率,其底层逻辑是什么?换句话说,将大问题分解为多个子问题、解决子问题、将子问题的解合并为原问题的解,这几步的效率为什么比直接解决原问题的效率更高?这个问题可以从操作数量和并行计算两方面来讨论。

操作数量优化

并行计算优化

我们知道,分治生成的子问题是相互独立的,因此通常可以并行解决。也就是说,分治不仅可以降低算法的时间复杂度,还有利于操作系统的并行优化

并行优化在多核或多处理器的环境中尤其有效,因为系统可以同时处理多个子问题,更加充分地利用计算资源,从而显著减少总体的运行时间。

比如在下图所示的“桶排序”中,我们将海量的数据平均分配到各个桶中,则可将所有桶的排序任务分散到各个计算单元,完成后再合并结果。

分治常见应用

一方面,分治可以用来解决许多经典算法问题。

  • 寻找最近点对:该算法首先将点集分成两部分,然后分别找出两部分中的最近点对,最后找出跨越两部分的最近点对。
  • 大整数乘法:例如 Karatsuba 算法,它将大整数乘法分解为几个较小的整数的乘法和加法。
  • 矩阵乘法:例如 Strassen 算法,它将大矩阵乘法分解为多个小矩阵的乘法和加法。
  • 汉诺塔问题:汉诺塔问题可以通过递归解决,这是典型的分治策略应用。
  • 求解逆序对:在一个序列中,如果前面的数字大于后面的数字,那么这两个数字构成一个逆序对。求解逆序对问题可以利用分治的思想,借助归并排序进行求解。

另一方面,分治在算法和数据结构的设计中应用得非常广泛。

  • 二分查找:二分查找是将有序数组从中点索引处分为两部分,然后根据目标值与中间元素值比较结果,决定排除哪一半区间,并在剩余区间执行相同的二分操作。
  • 归并排序:本节开头已介绍,不再赘述。
  • 快速排序:快速排序是选取一个基准值,然后把数组分为两个子数组,一个子数组的元素比基准值小,另一子数组的元素比基准值大,再对这两部分进行相同的划分操作,直至子数组只剩下一个元素。
  • 桶排序:桶排序的基本思想是将数据分散到多个桶,然后对每个桶内的元素进行排序,最后将各个桶的元素依次取出,从而得到一个有序数组。
  • :例如二叉搜索树、AVL 树、红黑树、B 树、B+ 树等,它们的查找、插入和删除等操作都可以视为分治策略的应用。
  • :堆是一种特殊的完全二叉树,其各种操作,如插入、删除和堆化,实际上都隐含了分治的思想。
  • 哈希表:虽然哈希表并不直接应用分治,但某些哈希冲突解决方案间接应用了分治策略,例如,链式地址中的长链表会被转化为红黑树,以提升查询效率。

可以看出,分治是一种“润物细无声”的算法思想,隐含在各种算法与数据结构之中。

案例

二分查找

public static int binarySearch(int[] a, int target) {
     return recursion(a, target, 0, a.length - 1);
 }
 
 public static int recursion(int[] a, int target, int i, int j) {
     if (i > j) {
         return -1;
     }
     int m = (i + j) >>> 1;
     if (target < a[m]) {
         return recursion(a, target, i, m - 1);
     } else if (a[m] < target) {
         return recursion(a, target, m + 1, j);
     } else {
         return m;
     }
 }

减而治之,每次搜索范围内元素减少一半

快速排序

public static void sort(int[] a) {
     quick(a, 0, a.length - 1);
 }
 
 private static void quick(int[] a, int left, int right) {
     if (left >= right) {
         return;
     }
     int p = partition(a, left, right);
     quick(a, left, p - 1);
     quick(a, p + 1, right);
 }

分而治之,这次分区基准点,在划分后两个区域分别进行下次分区

归并排序

public static void sort(int[] a1) {
     int[] a2 = new int[a1.length];
     split(a1, 0, a1.length - 1, a2);
 }
 
 private static void split(int[] a1, int left, int right, int[] a2) {
     int[] array = Arrays.copyOfRange(a1, left, right + 1);
     // 2. 治
     if (left == right) {
         return;
     }
     // 1. 分
     int m = (left + right) >>> 1;
     split(a1, left, m, a2);                 
     split(a1, m + 1, right, a2);       
     // 3. 合
     merge(a1, left, m, m + 1, right, a2);
     System.arraycopy(a2, left, a1, left, right - left + 1);
 }

分而治之,分到区间内只有一个元素,合并区间

合并K个排序链表 - LeetCode 23

public ListNode mergeKLists(ListNode[] lists) {
     if (lists.length == 0) {
         return null;
     }
     return split(lists, 0, lists.length - 1);
 }
 
 public ListNode split(ListNode[] lists, int i, int j) {
     System.out.println(i + " " + j);
     if (j == i) {
         return lists[i];
     }
     int m = (i + j) >>> 1;
     return mergeTwoLists(
         split(lists, i, m),
         split(lists, m + 1, j)
     );
 }

分而治之,分到区间内只有一个链表,合并区间

快速选择算法

public class Utils {
     static int quick(int[] a, int left, int right, int index) {
         int p = partition(a, left, right);
         if (p == index) {
             return a[p];
         }
         if (p < index) {
             return quick(a, p + 1, right, index);
         } else {
             return quick(a, left, p - 1, index);
         }
     }
 
     static int partition(int[] a, int left, int right) {
         int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
         swap(a, left, idx);
         int pv = a[left];
         int i = left + 1;
         int j = right;
         while (i <= j) {
             // i 从左向右找大的或者相等的
             while (i <= j && a[i] < pv) {
                 i++;
             }
             // j 从右向左找小的或者相等的
             while (i <= j && a[j] > pv) {
                 j--;
             }
             if (i <= j) {
                 swap(a, i, j);
                 i++;
                 j--;
             }
         }
         swap(a, j, left);
         return j;
     }
 
     static void swap(int[] a, int i, int j) {
         int t = a[i];
         a[i] = a[j];
         a[j] = t;
     }
 }

数组中第k个最大元素-Leetcode 215

public class FindKthLargestLeetcode215 {
     /*
         目标 index = 4
             3   2   1   5   6   4
         =>  3   2   1   4   5   6   (3)
         =>  3   2   1   4   5   6   (5)
         =>  3   2   1   4   5   6   (4)
      */
 
     public int findKthLargest(int[] a, int k) {
         return Utils.quick(a, 0, a.length - 1, a.length - k);
     }
 
     public static void main(String[] args) {
         // 应为5
         FindKthLargestLeetcode215 code = new FindKthLargestLeetcode215();
         System.out.println(code.findKthLargest(new int[]{3, 2, 1, 5, 6, 4}, 2));
         // 应为4
         System.out.println(code.findKthLargest(new int[]{3, 2, 3, 1, 2, 4, 5, 5, 6}, 4));
     }
 }

数组中位数

public class FindMedian {
     /*
         偶数个
             3   1   5   4
         奇数个
             4   5   1
             4   5   1   6   3
      */
     public static double findMedian(int[] nums) {
         if (nums.length % 2 != 0) {
             return findIndex(nums, nums.length / 2);
         } else {
             System.out.println((nums.length / 2 - 1) + "," + (nums.length / 2));
             int a = findIndex(nums, nums.length / 2);
             int b = findIndex(nums, nums.length / 2 - 1);
             return (a + b) / 2.0;
         }
     }
 
     public static void main(String[] args) {
         System.out.println(findMedian(new int[]{3, 1, 5, 4}));
         System.out.println(findMedian(new int[]{3, 1, 5, 4, 7, 8}));
         System.out.println(findMedian(new int[]{4, 5, 1}));
         System.out.println(findMedian(new int[]{4, 5, 1, 6, 3}));
     }
 
     static int findIndex(int[] a, int index) {
         return Utils.quick(a, 0, a.length - 1, index);
     }
 
 }

快速幂-Leetcode 50

public class QuickPowLeetcode50 {
 
     /*
                   2^10
               /         \
             2^5         2^5
            /  \        /  \
         2 2^2 2^2    2 2^2 2^2
          / \  / \     / \  / \
         2  2  2  2   2  2  2  2
 
 
                   256          n=1 x=65536 mul=1024
               /         \
             16          16          n=2 x=256 mul=4
            /  \        /  \
         2 4    4    2  4    4       n=5  x=16 mul=4
          / \  / \     / \  / \
         2  2  2  2   2  2  2  2     n=10  x=4  mul=1
 
      */
 
     
     static double myPow(double x, int n) {
         if (n == 0) {
             return 1;
         }
         double mul = 1;
         long N = n;
         if (n < 0) {
             N = -N;
         }
         while (N > 0) {
             if ((N & 1) == 1) {
                 mul *= x;
             }
             x =  x * x;
             N = N >> 1;
         }
         return n > 0 ? mul : 1 / mul;
     }
     
     static double myPow1(double x, int n) {
         long N = n;
         if (N < 0) {
             return 1.0 / rec(x, -N);
         }
         return rec(x, n);
     }
 
     static double rec(double x, long n) {
         if (n == 0) {
             return 1;
         }
         if (n == 1) {
             return x;
         }
         double y = rec(x, n / 2);
         if ((n & 1) == 1) {
             return x * y * y;
         }
         return y * y;
     }
 
     public static void main(String[] args) {
         System.out.println(myPow(2, 10));  // 1024.0
         System.out.println(myPow(2.1, 3)); // 9.261
         System.out.println(myPow(2, -2)); // 0.25
         System.out.println(myPow(2, 0)); // 1.0
         System.out.println(myPow(2, -2147483648)); // 1.0
     }
 }

平方根整数部分-Leetcode 69

public class SqrtLeetcode69 {
     static int mySqrt(int x) {
         int i = 1, j = x;
         int r = 0;
         while (i <= j) {
             int m = (i + j) >>> 1;
             if (x / m >= m) {
                 r = m;
                 i = m+1;
             } else {
                 j = m-1;
             }
         }
         return r;
     }
 
     public static void main(String[] args) {
         System.out.println(mySqrt(1));
         System.out.println(mySqrt(2));
         System.out.println(mySqrt(4));
         System.out.println(mySqrt(8));
         System.out.println(mySqrt(9));
     }
 }
  • while(i <= j) 含义是在此区间内,只要有数字还未尝试,就不算结束
  • r 的作用是保留最近一次当 m^2 <= x 的 m 的值
  • 使用除法而非乘法,避免大数相乘越界

至少k个重复字符的最长子串-Leetcode 395

public class LongestSubstringLeetcode395 {
 
     static int longestSubstring(String s, int k) {
         // 子串落选情况
         if (s.length() < k) {
             return 0;
         }
         int[] counts = new int[26]; // 索引对应字符 值用来存储该字符出现了几次
         char[] chars = s.toCharArray();
         for (char c : chars) { // 'a' -> 0  'b' -> 1 ....
             counts[c - 'a']++;
         }
         System.out.println(Arrays.toString(counts));
         for (int i = 0; i < chars.length; i++) {
             char c = chars[i];
             int count = counts[c - 'a']; // i字符出现次数
             if (count > 0 && count < k) {
                 int j = i + 1;
                 while(j < s.length() && counts[chars[j] - 'a'] < k) {
                     j++;
                 }
                 System.out.println(s.substring(0, i) + "\t" + s.substring(j));
                 return Integer.max(
                         longestSubstring(s.substring(0, i), k),
                         longestSubstring(s.substring(j), k)
                 );
             }
         }
         // 子串入选情况
         return s.length();
     }
 
     public static void main(String[] args) {
         //                                         i j
         System.out.println(longestSubstring("aaaccbbb", 3)); // ababb
         System.out.println(longestSubstring("dddxaabaaabaacciiiiefbff", 3));
 //        System.out.println(longestSubstring("ababbc", 3)); // ababb
 //        System.out.println(longestSubstring("ababbc", 2)); // ababb
         /*
             ddd aabaaabaa iiii fbff
                 aa aaa aa      f ff
 
             统计字符串中每个字符的出现次数,移除哪些出现次数 < k 的字符
             剩余的子串,递归做此处理,直至
                  - 整个子串长度 < k (排除)
                  - 子串中没有出现次数 < k 的字符
          */
     }
 }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

埃泽漫笔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值