算法设计思想 本章学习要求 :掌握蛮力、分治、减治、变治和时空权衡等算法设计思想的特点。
蛮力法-选择排序、冒泡排序 选择排序 :每次遍历未排序的整个列表,选择一个最小的与第一卦没有被交换的进行交换,下次从被交换的下一个元素开始遍历。
public class SelectSort { public static void main (String[] args) { int [] arr = {10 , 8 , 7 , 5 , 6 , 1 , 2 , 3 , 9 , 4 }; selectionSort(arr); } public static void selectionSort (int [] arr) { int n = arr.length; for (int i = 0 ; i < n - 1 ; i++) { int minIndex = i; for (int j = i + 1 ; j < n; j++) { if (arr[j] < arr[minIndex]) { minIndex = j; } } if (minIndex != i) { int temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; } } } }
冒泡排序 :从第一个元素开始遍历列表直到不确定位置的最后一个元素,如果相邻元素是逆序则交换位置,每次遍历后,有一个元素会被放到对应的位置上。
public static void bubbleSort (int [] array) { if (array == null || array.length <= 1 ){ return ; } int n = array.length; boolean swapped; for (int i = 0 ; i < n - 1 ; i++){ swapped = false ; for (int j = 0 ; j < n - 1 - i; j++){ if (array[j] > array[j + 1 ]){ int temp = array[j]; array[j] = array[j + 1 ]; array[j + 1 ] = temp; swapped = true ; } } if (!swapped){ break ; } } }
例题1 :设计一个蛮力算法对给定的x计算多项式值。
public static void main (String[] args) { int [] nums = {1 , 2 , 3 , 4 , 5 }; int x = 2 ; int res = 0 ; for (int i = 0 ; i < nums.length; i++){ res += nums[i] * (int )Math.pow(x, i); } return res; }
例题2 :求一个数组中大小最接近的两个元素的差的算法。
public static void main (String[] args) { int [] arr = {10 , 8 , 7 , 5 , 6 , 1 , 2 , 3 , 9 , 4 }; Arrays.sort(arr); int minDiff = Integer.MAX_VALUE; for (int i = 0 ; i < arr.length - 1 ; i++){ int diff = arr[i+1 ] - arr[i]; if (diff < minDiff){ minDiff = diff; } } System.out.println(minDiff); }
分治法-归并排序、快速排序 将问题实例划分为同一问题的几个较小的实例,对这些较小的实例进行求解,最后合并这些较小问题实例的解得到原问题的解。
问题规模缩小到一定程度就很容易解决
问题能够划分为多个相互独立的子问题
归并排序 :将一个需要排序的数组一分为两个子数组,分别对两个子数组进行排序,最后将两个子数组合并。
public class MergeSort { public static void mergeSort (int [] arr) { if (arr == null || arr.length <= 1 ) { return ; } int [] temp = new int [arr.length]; mergeSort(arr, 0 , arr.length - 1 , temp); } private static void mergeSort (int [] arr, int left, int right, int [] temp) { if (left < right) { int mid = left + (right - left) / 2 ; mergeSort(arr, left, mid, temp); mergeSort(arr, mid + 1 , right, temp); merge(arr, left, mid, right, temp); } } private static void merge (int [] arr, int left, int mid, int right, int [] temp) { int i = left; int j = mid + 1 ; int k = left; while (i <= mid && j <= right) { if (arr[i] <= arr[j]) { temp[k++] = arr[i++]; } else { temp[k++] = arr[j++]; } } while (i <= mid) { temp[k++] = arr[i++]; } while (j <= right) { temp[k++] = arr[j++]; } for (k = left; k <= right; k++) { arr[k] = temp[k]; } } public static void main (String[] args) { int [] arr = {5 , 3 , 8 , 4 , 2 , 7 , 1 , 6 }; System.out.println("排序前的数组:" ); for (int num : arr) { System.out.print(num + " " ); } mergeSort(arr); System.out.println("\n排序后的数组:" ); for (int num : arr) { System.out.print(num + " " ); } } }
快速排序 :通过选择一个基准元素(pivot),将数组分为两部分,使得左边部分的所有元素都小于等于基准元素,右边部分的所有元素都大于等于基准元素,然后分别对左右两部分递归地进行快速排序,最终得到一个有序的数组。
public class QuickSort { public static void quickSort (int [] arr) { if (arr == null || arr.length <= 1 ) { return ; } quickSort(arr, 0 , arr.length - 1 ); } private static void quickSort (int [] arr, int left, int right) { if (left < right) { int pivotIndex = partition(arr, left, right); quickSort(arr, left, pivotIndex - 1 ); quickSort(arr, pivotIndex + 1 , right); } } private static int partition (int [] arr, int left, int right) { int pivot = arr[right]; int i = left - 1 ; for (int j = left; j < right; j++) { if (arr[j] <= pivot) { i++; swap(arr, i, j); } } swap(arr, i + 1 , right); return i + 1 ; } private static void swap (int [] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } public static void main (String[] args) { int [] arr = {5 , 3 , 8 , 4 , 2 , 7 , 1 , 6 }; System.out.println("排序前的数组:" ); for (int num : arr) { System.out.print(num + " " ); } quickSort(arr); System.out.println("\n排序后的数组:" ); for (int num : arr) { System.out.print(num + " " ); } } }
例题1 :有一个由 N 个实数构成的数组,如果一对元素 A[i] 和 A[j] 是倒序的,即 i < j 但是 A[i] > A[j] 则称它们是一个倒置,设计一个计算该数组中所有倒置数量的算法。要求算法复杂度为 O(nlogn)。
public class InversionCount { public static int countInversions (int [] arr) { int [] temp = new int [arr.length]; return mergeSortAndCount(arr, temp, 0 , arr.length - 1 ); } private static int mergeSortAndCount (int [] arr, int [] temp, int left, int right) { int invCount = 0 ; if (left < right) { int mid = (left + right) / 2 ; invCount += mergeSortAndCount(arr, temp, left, mid); invCount += mergeSortAndCount(arr, temp, mid + 1 , right); invCount += mergeAndCount(arr, temp, left, mid, right); } return invCount; } private static int mergeAndCount (int [] arr, int [] temp, int left, int mid, int right) { int i = left; int j = mid + 1 ; int k = left; int invCount = 0 ; while (i <= mid && j <= right) { if (arr[i] <= arr[j]) { temp[k++] = arr[i++]; } else { temp[k++] = arr[j++]; invCount += (mid - i + 1 ); } } while (i <= mid) { temp[k++] = arr[i++]; } while (j <= right) { temp[k++] = arr[j++]; } for (i = left; i <= right; i++) { arr[i] = temp[i]; } return invCount; } }
减治法-插入排序 利用一个问题给定实例的解和同样问题较小实例的解之间的某种关系,将一个大规模的问题逐步化简为一个小规模的问题。减治法和分治法的区别和联系 :
分治:是多个小问题,小问题之间的联系
减治:还是一个小问题,小问题与原问题之间的联系
插入排序 :假设前 n - 1 个元素以已经排序好了,如何将最后一个元素插入到其他元素中去的问题。
public static void insertionSort (int [] arr) { int n = arr.length; for (int i = 1 ; i < n; i++) { int key = arr[i]; int j = i - 1 ; while (j >= 0 && arr[j] > key) { arr[j + 1 ] = arr[j]; j--; } arr[j + 1 ] = key; } }
例题1 :N 个士兵过又宽又深的河,没有桥,不用船就会牺牲;发现有 2 个小朋友在划船,但是这个船只能搭两个小朋友或 1 个士兵,如何过河?需要来回多少次?
每次只能容纳一名士兵,所以士兵一定是一个一个过河,同时需要有小男孩将船划回来,那么每一次士兵过河之前,两个小男孩先划船过去对岸,然后留下一个小男孩在对岸,另一个小男孩划船回来。接着一个士兵过河,另一个小男孩在士兵过河之后把船划回来。然后继续重复,两个小男孩划船过去对岸……所以摆渡 n 个士兵,需要执行这样的步骤 n 次,每次船在岸与岸之间横渡 4 次,共 4n 次。
例题2 :有 2n 个玻璃杯挨个排成一排,前 n 个装满苏打水,其余n个杯子为空。请交换杯子位置,使得按照满-空-满-空-满…的顺序排放。要求交换次数最少。
n 为奇数的时候,空杯从第二个开始,每隔一个满杯与之交换;n 为偶数的时候,空杯从第一个开始,每隔一个满杯与之交换。
例题3 :设计一个减一算法,生成一个 n 个元素集合的幂集(集合 S 的幂集是 S 的所有子集的集合,包括空集和 S 本身)。
减一算法是一种迭代算法,它通过移除集合中的一个元素来生成子集。减一算法的基本思想是通过二进制数的所有可能组合来表示集合的所有子集。对于一个具有 n 个元素的集合,其幂集的元素个数为 2^n,可以用从 0 到 2^n - 1 的二进制数来表示每个子集,其中二进制数的每一位对应集合中的一个元素,如果该位为 1,则表示该元素在子集中,否则不在子集中。
例题4 :Shell排序,它是插入排序的一种更高效的改进版本,也称为缩小增量排序。它的基本思想是将原始数据分成多个子序列来进行插入排序,通过逐渐缩小子序列的间隔(增量),最终使整个序列基本有序,最后进行一次整体的插入排序。
public static void shellSort (int [] arr) { int n = arr.length; for (int gap = n / 2 ; gap > 0 ; gap /= 2 ) { for (int i = gap; i < n; i++) { int temp = arr[i]; int j; for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) { arr[j] = arr[j - gap]; } arr[j] = temp; } } }
变治法-预排序 通过转换问题使得原问题更容易求解。预排序 :一种变治的思想,先对输入列表进行排序,再求解原问题。例题1 :有 n 个数字构成的一个数组和一个整数 s,如何最快确定数组中是否存在两个数的和恰好等于 s。
public class TwoSumWithSorting { public static boolean hasTwoSum (int [] nums, int s) { Arrays.sort(nums); int left = 0 ; int right = nums.length - 1 ; while (left < right) { int sum = nums[left] + nums[right]; if (sum == s) { return true ; } else if (sum < s) { left++; } else { right--; } } return false ; } }
例题2 :假设有两个数字的集合 A 和 B。现在要求两个集合的交集。请给出一个蛮力的方案,再用预排序的方法设计一个方案。
蛮力法:遍历 A 集合,用当前数字与 B 集合中所有数字比较,如果相同就加入交集,直到 A 遍历结束;预排序:分别排序,两个指针分别遍历,相等加入交集,都移动,哪个小移动哪个。
例题3 :在实数域上有 n 个开区间,开区间由严格位于区间端点之间的所有点构成,求给定开区间集合中包含共同点的区间的最大数量。例如 (1,4) , (0,3) , (-1.5,2) , (3.6,5),包含共同点的最大区间个数为 3 个。
class Interval { double start; double end; public Interval (double start, double end) { this .start = start; this .end = end; } } public class MaxOverlappingIntervalsUsingPQ { public static int maxOverlappingIntervals (List<Interval> intervals) { if (intervals == null || intervals.size() == 0 ) { return 0 ; } Collections.sort(intervals, Comparator.comparingDouble(i -> i.start)); PriorityQueue<Double> pq = new PriorityQueue <>(); int maxOverlap = 0 ; for (Interval interval : intervals) { while (!pq.isEmpty() && pq.peek() <= interval.start) { pq.poll(); } pq.offer(interval.end); maxOverlap = Math.max(maxOverlap, pq.size()); } return maxOverlap; } public static void main (String[] args) { List<Interval> intervals = new ArrayList <>(); intervals.add(new Interval (1 , 4 )); intervals.add(new Interval (0 , 3 )); intervals.add(new Interval (-1.5 , 2 )); intervals.add(new Interval (3.6 , 5 )); int result = maxOverlappingIntervals(intervals); System.out.println("包含共同点的最大区间个数为: " + result); } }
例题4 :在一个给定的单词集合中,如何快速查找所有变位词?例如 eat、ate、tea 属于同一个变位词集合。
public class AnagramFinder { public static List<List<String>> findAnagrams (List<String> words) { Map<String, List<String>> anagramMap = new HashMap <>(); for (String word : words) { char [] charArray = word.toCharArray(); Arrays.sort(charArray); String sortedWord = new String (charArray); if (!anagramMap.containsKey(sortedWord)) { anagramMap.put(sortedWord, new ArrayList <>()); } anagramMap.get(sortedWord).add(word); } return new ArrayList <>(anagramMap.values()); } public static void main (String[] args) { List<String> words = Arrays.asList("eat" , "ate" , "tea" , "tan" , "nat" , "bat" ); List<List<String>> anagramGroups = findAnagrams(words); for (List<String> group : anagramGroups) { System.out.println(group); } } }
时空权衡-计数排序 空间换时间,对问题的部分或者全部的输入作预处理,然后对获得的额外信息进行存储,以加速后面问题的求解。计数排序 :多次扫描列表,利用额外空间记录比每一个元素小的元素的个数,最后把原列表的元素复制到新数组对应下标地方。例题1 :写一个算法计算两个稀疏矩阵的乘积。
class SparseMatrix { int rows; int cols; List<int []> elements; public SparseMatrix (int rows, int cols, List<int []> elements) { this .rows = rows; this .cols = cols; this .elements = elements; } public SparseMatrix multiply (SparseMatrix other) { if (this .cols != other.rows) { throw new IllegalArgumentException ("矩阵维度不匹配,无法相乘" ); } List<int []> resultElements = new ArrayList <>(); for (int [] row : this .elements) { for (int [] col : other.elements) { if (row[1 ] == col[0 ]) { int dotProduct = row[2 ] * col[2 ]; if (dotProduct != 0 ) { resultElements.add(new int []{row[0 ], col[1 ], dotProduct}); } } } } return new SparseMatrix (this .rows, other.cols, resultElements); } }
例题1 :使用多种排序方法对 2^31 个 IPv4 地址进行排序;只有 1G 内存空间可以使用。
外部归并排序,依次将每个小文件加载到内存中,使用快速排序、堆排序等高效的内部排序算法对小文件内的地址进行排序;使用多路归并算法,同时从多个已排序的小文件中读取数据,每次选取最小的地址输出到一个新的文件中,直到所有小文件的数据都处理完毕。最终得到一个完整的、按顺序排列的 IPv4 地址文件。
蛮力法 本章学习要求 :能够熟练使用蛮力法解决问题,作为优化的基线方法,例如找到数组中大小最接近的两个元素的差、找到凸包极点等问题。
前言 蛮力法即枚举法、穷举法,暴力法。它的关键是依次处理所有元素或者依次处理所有可能的情况。 体现蛮力法的两个典型场景 :选择排序和冒泡排序
选择排序:选择最大或最小与第一个没有排序好的交换
冒泡排序:遍历列表,两两按照大小交换位置
蛮力法之顺序查找 在给定的列表中查找一个给定的值。蛮力解法 :给定的列表是搜索空间,需要在该空间内根据限制条件顺序查找。示例1 :求所有的三位数,它除以11所得的余数等于它的三位数字的平方和。
public class Main { public static void main (String[] args) { for (int num = 100 ; num < 1000 ; num++) { int remainder = num % 11 ; int hundreds = num / 100 ; int tens = (num / 10 ) % 10 ; int units = num % 10 ; int squareSum = hundreds * hundreds + tens * tens + units * units; if (remainder == squareSum) { System.out.println(num); } } } }
示例2 :某公司有N层楼,为了测出哪层楼最高,可以使用一种仪器从天花板向地板自由落体测时间,仪器不会摔坏;如果有两个一模一样的仪器,请设计一个最佳效率类型的算法解决这个问题。 课件中的翻译有误,实际上是:一家公司想要确定其N层总部大楼中,小部件从多高楼层掉落而不会摔坏的最高楼层。该公司有两个完全相同的小部件用于试验。如果其中一个摔坏了,就无法修复,后续试验将只能用剩下的那个小部件完成。设计一种算法来解决这个问题,使其尽可能达到最佳效率类别。
二分法:从 N/2 层扔一次,如果摔坏了,就从 1 楼开始逐个扔直到 N/2 层,反之从 N/2 层开始逐个扔直到顶层。
蛮力法之模式匹配 从n个字符组成的串中寻找匹配模式( m 个字符,m <= n)的子串。蛮力解法 :以文本中的每一个字符为开始,用模式串匹配,直到文本结束。示例1 :给定一个文本 CADFBAAXBYA,计算以 A 开始以 B 结束的字符串个数。
public class Main { public static void main (String[] args) { String text = "CADFBAAXBYA" ; int count = 0 ; for (int i = 0 ; i < text.length(); i++) { if (text.charAt(i) == 'A' ) { for (int j = i + 1 ; j < text.length(); j++) { if (text.charAt(j) == 'B' ) { count++; } } } } System.out.println(count); } }
蛮力法之最近对问题 找出一个包含 n 个点的集合中距离最近的两个点。蛮力解法 :求出每两个点之间的距离,选出最小的那一个。注意不要重复计算两个点之间的距离,即 A 和 B 的距离与 B 和 A 的距离是等价的。示例1 :一条线上的 n 个点如何计算距离最小的两个点。
public class ClosestPointsIndices { public static int [] findClosestPointsIndices(int [] points) { int n = points.length; if (n < 2 ) { return new int []{-1 , -1 }; } int minDistance = Integer.MAX_VALUE; int index1 = -1 ; int index2 = -1 ; for (int i = 0 ; i < n; i++) { for (int j = i + 1 ; j < n; j++) { int distance = Math.abs(points[i] - points[j]); if (distance < minDistance) { minDistance = distance; index1 = i; index2 = j; } } } return new int []{index1, index2}; } }
蛮力法之凸包问题 为一个包含 n 个点的集合构造凸包的问题。
凸包 :一个点集合 S 的凸包是包含 S 的最小凸集合
凸集合 :对面平面上的一个点集合,如果以集合中任意两点 P 和 Q 为端点的线段都属于该集合,我们就称这个集合是凸的(例如直线、三角形)
凸包定理 :任意包含多于两个不共线点的集合 S 的凸包肯定是以 S 中某些点为顶点的凸多边形
示例1 :设计一个线性算法找到 n>1 个点集合的两个极点(凸包的顶点)。
极点的特性:如果两个点构成的线段是凸包的边界,则其他点都在这条线段所在直线的一边
示例2 :线性规划问题,求出 3x + 5y 在约束条件 x + y <= 4 且 x + 3y <= 6 且 x >= 0,y >= 0下的最大值。
约束条件构成可行区域即凸包,最优解一定出现在凸包的某个极点上public class LinearProgrammingBruteForce { public static void main (String[] args) { double step = 0.01 ; double maxValue = 0 ; for (double x = 0 ; x <= 4 ; x += step) { for (double y = 0 ; y <= 4 ; y += step) { if (x + y <= 4 && x + 3 * y <= 6 && x >= 0 && y >= 0 ) { double currentValue = 3 * x + 5 * y; if (currentValue > maxValue) { maxValue = currentValue; } } } } System.out.println("3x + 5y 的最大值是: " + maxValue); } }
蛮力法之穷举查找 用户在指数级增长的解空间中查找满足要求的解。示例1 :旅行商问题,即 TSP 问题。有一个旅行商从城市 1 出发,需要到城市 2……n 去推销,最后返回城市 1,任意两个城市的距离已知,求其最佳行走路线。
旅行商问题的扩展:瓶颈 TSP,即经过的最长距离最短
旅行商问题的扩展:最小比率 TSP,即除了行程还会产生收益,优化的目标是总行程与总收益比最小
旅行商问题的扩展:多人 TSP,即多个推销员同时出发,路线不同,使得所有城市至少被访问一次,优化的目标是所有人的总路程最小
示例2 :背包问题,给定 n 个,重量为 w1……wn,价值为 v1……vn 的物品和一个承重为 W 的背包,求这些物品中一个最有价值的子集,并且装到背包中。
背包问题的扩展:完全背包,即每一件物品不计件数
背包问题的扩展:多重背包,即每一件物品对应有一个确定的件数
示例3 :分配问题,将 n 个任务分配给 n 个人执行,每个人只执行一个任务,每个任务只能给一个人执行。将第 j 个任务分配个第 i 个人执行的成本是 C[i,j],如何找到总成本最小的分配方案。
分配问题蛮力解法
分配问题匈牙利算法(时间复杂度通常是 O(n^3))
示例4 :分配问题之二分图的最大匹配,在二分图中,顶点被分为两个不相交的集合,且图中的每条边都连接这两个集合中一个顶点,最大匹配指的是选取尽可能多的边,且这些边之间没有公共顶点。使用增广路径算法。
二分图最大匹配的扩展:加权二分图的最大匹配,即图中的每一条边有一个权重,在满足完全匹配(最大匹配)的前提下,选择权重和最大的方式
蛮力法其他例题 假金币问题 :有 N 个金币,其中一个是假的,重量小于真金币,其他真金币的重量都相同,只有一台天平可以使用,设计一个算法找到假金币,或者给定测试结果,判断假金币编号。
只有一个金币是假的,根据第二行结果可知编号为 2 和 5 的金币都为真金币,根据第三行结果可知编号为 1 和 4 的金币都为真金币,则假金币是编号为 3 的金币。
阶乘位数问题 :计算 N! 有多少位。
分治法 本章学习要求 :分治思想在具体实现的时候,可以使用递归的方法,也可以结合特殊的数据结构使用循环的方法,例如合并排序、快速排序等。能够采用分治思想解决的两个经典问题是最近对问题和凸包问题,请掌握具体的代码实现。
前言 分治法是最著名的通用算法设计技术,很多有效的算法是分治算法的特殊实现。 分治法的具体方案流程:
将问题实例划分为同一个问题的几个较小实例,最好拥有相同规模
对每一个较小规模的实例进行求解
如果需要则以某种方式合并这些小问题的解得到原问题的解
分治法在排序中的应用:归并排序 和快速排序 。例题1 :N 个直径各不相同的螺母和 N 个直径各不相同的螺钉,但是螺钉和螺母是一一对应的。如果不可以使用螺母与螺母比较,也不能使用螺钉和螺钉比较,如何快速找到它们的对应关系?
从螺钉中选出一个,对螺母们进行划分成三部分:比这个螺钉小的、大的和相等的。随后取出第一步相等的螺母,对螺钉们进行相同的划分。对小的部分,和大的部分,进行递归操作。
例题2 :Tromino 是一个由棋盘上的三个邻接方块组成的 L 型瓦片,我们的问题是,如何用 Tromino 覆盖一个缺少了一个方块的 2 ^ n * 2 ^ n 的棋盘,除了这个缺失的方块,要覆盖棋盘上所有其它方块,而且不能有重复。
通过对问题的分析,题目要求用分治法来解决该问题。该问题中缺失方块的位置是任意的,棋盘大小是 2 的 n 次方(n 为正整数)的矩阵,所以先来考虑最小规模即当 n = 1 时的情况。这种情况下无论缺失的方块在哪个位置,只需要将剩下的三个方块填充就好,相当于放置一个骨牌。当 n = 2 时,方块数为4 * 4。划分子问题前,我们先将棋盘分为四个象限,确定缺失方块的象限后,将其它三个象限距离中心位置最近的一个方块填充。此时再将其划分为四个方块数为2 * 2 的矩阵,将已经填充的方块看作缺失方块,则每个小规模矩阵都有一个缺失方块,即它们是规模相同的子问题,依次递归最后将整个棋盘填充完整。
分治法之折半查找 对于有序列表的卓越查找方案。通过对比查找键值与列表中间元素的大小,确定下一次查找的位置。分治法折半查找递归方案 :
public class BinarySearchRecursive { public static int binarySearch (int [] arr, int left, int right, int target) { if (left > right) { return -1 ; } int mid = left + (right - left) / 2 ; if (arr[mid] == target) { return mid; } else if (arr[mid] > target) { return binarySearch(arr, left, mid - 1 , target); } else { return binarySearch(arr, mid + 1 , right, target); } } }
分治法折半查找非递归方案 :
public class BinarySearch { public static int binarySearch (int [] array, int target) { int left = 0 ; int right = array.length - 1 ; while (left <= right) { int mid = left + (right - left) / 2 ; if (array[mid] == target) { return mid; } if (array[mid] > target) { right = mid - 1 ; } else { left = mid + 1 ; } } return -1 ; } }
分治法之二叉树遍历 回顾完全二叉树、满二叉树等二叉树基本概念:
完全二叉树:如果有 h 层,1 ~ h-1 层布满节点,h 层节点从左向右分布
满二叉树:除了叶子节点其它节点都有左右子节点,且叶子节点都在最底层
二叉查找树:排序好二叉树,如果左子树不空,则左子树值小于根节点值;如果右子树不空,则右子树值都大于根节点;左右子树分别是二叉查找树
平衡二叉树:是一棵左右子树高度差绝对值不大于 1 的二叉查找树
二叉树与分治思想的关联在于,对二叉树的操作可以变为对左右子树的分别操作。但不是所有二叉树的操作都体现分治思想,例如二叉查找树的插入和删除等。分治法之计算二叉树高度 :左子树的最大高度或右子树的最大高度 + 1。
public class BinaryTreeHeight { public int getHeight (TreeNode root) { if (root == null ) { return 0 ; } int leftHeight = getHeight(root.left); int rightHeight = getHeight(root.right); return Math.max(leftHeight, rightHeight) + 1 ; } }
分治法之二叉树的遍历 :前序遍历(根-左-右),中序遍历(左-根-右),后序遍历(左-右-根)。
public class BinaryTreeTraversal { public static void preOrderTraversal (TreeNode root) { if (root == null ) { return ; } System.out.print(root.val + " " ); preOrderTraversal(root.left); preOrderTraversal(root.right); } public static void inOrderTraversal (TreeNode root) { if (root == null ) { return ; } inOrderTraversal(root.left); System.out.print(root.val + " " ); inOrderTraversal(root.right); } public static void postOrderTraversal (TreeNode root) { if (root == null ) { return ; } postOrderTraversal(root.left); postOrderTraversal(root.right); System.out.print(root.val + " " ); } }
分治法之二叉树节点间最大距离 :可能的情况是,左子树中节点间最大距离,或者右子树中节点最大距离,或者左子树中节点到右子树中节点的最大距离。
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; } } private static class Distance { int maxDistance; int height; Distance(int maxDistance, int height) { this .maxDistance = maxDistance; this .height = height; } } public static int findMaxDistance (TreeNode root) { return findMaxDistanceUtils(root).maxDistance; } private static Distance findMaxDistanceUtils (TreeNode root) { if (root == null ) { return new Distance (0 , -1 ); } Distance leftDistance = findMaxDistanceUtils(root.left); Distance rightDistance = findMaxDistanceUtils(root.right); int throughRoot = leftDistance.height + rightDistance.height + 2 ; int withoutRoot = Math.max(leftDistance.maxDistance, rightDistance.maxDistance); int maxDistance = Math.max(throughRoot, withoutRoot); int height = Math.max(leftDistance.height, rightDistance.height) + 1 ; return new Distance (maxDistance, height); }
例题1 :有一个 n × m 格的巧克力,我们要把它掰成 n × m 个 1 × 1 的小块,我们只能沿着直线掰,而且不能几块同时掰,设计一个算法用最少的次数掰完巧克力,该次数是多少?如果可以几块同时掰,最少需要多少次?
每次都掰一块巧克力,当是一条 1 x m 的巧克力时,最少需要 m - 1 次,故对 n x m 的巧克力,我们可以先掰成 n 条 1 × m 的巧克力,则此时需要 n - 1 次,再对每个 1 x m 掰 m - 1 次,则此时总次数是 n × m - 1 次;当没有每次操作一块巧克力的限制时,我们还是对 1 x m 的巧克力考虑,此时需要 log m 次,而将 n × m 掰成 1 × m 需要 log n 次,故总次数是 log n + log m,此时掰的时候是将巧克力叠着一起掰。
分治法之大数乘法、斯特拉森矩阵乘法 密码技术中,需要对超过 100 位的十进制整数进行乘法运算,但这些整数无法用整型表示,衍生出大数乘法 问题。对于任意两个两位整数的例子 :把结果看成是由百位、十位、个位数字组成。
百位数字是两个两位数的十位数字相乘得到
个位数字是两个两位数的个位数字相乘得到
十位数字的算法稍复杂,是把两个两位数的十位与个位数字分别相加后相乘,再减去前面得到的百位数字与个位数字之和
大数乘法中的分治思想 :对于位数较多的整数相乘,用分治思想,就是把大问题变成小问题。如果整数的位数 n 是偶数,就把每个整数拆成两部分,前半部分乘 10 的 n 除以 2 次方,再加上后半部分。然后像算两位数乘法那样,把乘积结果也拆成三部分,计算这三部分的方法和两位数乘法类似:
第一部分类似两位数乘法里的百位数字算法
第二部分类似十位数字算法
第三部分类似个位数字算法
应用主定理计算复杂度 : 在大数乘法 算法中,a 为 3,b 为 2,d 为 1,所以根据主定理,该算法的时间复杂度为 O(n ^ 1.58),优于传统乘法的 O(n ^ 2)。
其中,a 代表递归子问题的数量
其中,b 代表子问题的规模
其中,d 代表非递归部分的时间复杂度
从大于 600 位的整数开始,分治算法的性能超越笔算算法的性能。 两个两阶矩阵的乘积计算需要 8 次乘法,Strassen 发现计算矩阵乘法可以只使用 7 次乘法。Strassen 矩阵乘法 的核心原理是通过将大矩阵分解为较小的子矩阵,并利用特定的组合方式来减少乘法运算的次数。算法步骤简略描述如下:
将输入的两个 n × n 矩阵 A 和 B 划分为四个 n / 2 × n / 2 的子矩阵
按照 Strassen 的方法,计算 7 个中间矩阵
根据 7 个中间矩阵,计算结果矩阵 C 的四个子矩阵
将计算得到的四个子矩阵组合起来,得到最终的结果矩阵 C
应用主定理计算复杂度 :
分治法之最近对问题 分治思想 在最近对问题中的应用:
使用一个坐标维度将点分为两个大小接近的集合,需要先排序
分别计算两个集合中的最近对
然后合并
对小集合中的点递归使用分治思想
分治法之凸包问题