一、概述
分治思想
- 将大问题划分为两个到多个子问题
- 子问题可以继续拆分成更小的子问题,直到能简单求解
- 如有必要,将子问题的解进行合并,得到原始问题的解
1. 二分查找
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;
}
}
减而治之,每次搜索范围内元素减少一半。
2. 快速排序
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);
}
分而治之,这次分区基准点,在划分之后两个区域分别进行下次分区。
3. 归并排序
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);
}
分而治之,分到区间内只有一个元素,合并区间。
4. 合并K个排序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]] 输出:[1,1,2,3,4,4,5,6] 解释:链表数组如下: [ 1->4->5, 1->3->4, 2->6 ] 将它们合并到一个有序链表中得到。 1->1->2->3->4->4->5->6
示例 2:
输入:lists = [] 输出:[]
示例 3:
输入:lists = [[]] 输出:[]
提示:
k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i]
按 升序 排列lists[i].length
的总和不超过10^4
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode s = new ListNode(-1, null);
ListNode p = s;
while (list1 != null && list2 != null) {
// 谁小把谁链给p,p和小的都向后平移一位
if (list1.val < list2.val) {
p.next = list1;
list1 = list1.next;
} else {
p.next = list2;
list2 = list2.next;
}
p = p.next;
}
// 处理剩余节点
if (list1 != null) {
p.next = list1;
}
if (list2 != null) {
p.next = list2;
}
return s.next;
}
public ListNode split(ListNode[] lists, int i, int j) {
if (j == i) {
return lists[i];
}
int m = (i + j) >>> 1;
return mergeTwoLists(split(lists, i, m), split(lists, m + 1, j));
}
public ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) {
return null;
}
return split(lists, 0, lists.length - 1);
}
}
分而治之,分到区间内只有一个链表,合并区间
5. 对比动态规划
- 都需要拆分子问题
- 动态规划的子问题有重叠,因此需要记录之前子问题解,避免重复运算
- 分而治之的子问题无重叠
二、快速选择算法
快速选择(Quickselect)算法是一种用于从未排序的列表中选择第 k 小(或第 k 大)元素的方法,它基于快速排序(Quicksort)算法的原理。其平均时间复杂度为 O(n),最坏情况下为 O(n^2),但在大多数情况下运行得很快。
package com.itheima.algorithms.divideandconquer;
import java.util.concurrent.ThreadLocalRandom;
/**
* 快速选择算法 - 分而治之
*/
public class QuickSelect {
/**
* 求排在第i名的元素,i从0开始,由小到大排
* 6 5 1 2 4
*/
public static int quick(int[] array, int left, int right, int i) {
/*
6 5 1 2 [4]
2
1 2 4 6 5
1 2 4 6 [5]
3
1 2 4 5 6
*/
// 基准点元素索引值
int p = partition(array, left, right);
if(p == i) {
return array[p];
}
if(i < p) {
// 到左边找
return quick(array, left, p - 1, i);
} else {
// 到右边找
return quick(array, p + 1, right, i);
}
}
private static int partition(int[] a, int left, int right) {
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
swap(a, idx, left);
int pivot = a[left];
int i = left + 1, j = right;
while (i <= j) {
// 2. i从左向右找大的
while (i <= j && a[i] < pivot) {
i++;
}
// 1. j从右向左找小(等)的
while (i <= j && a[j] > pivot) {
j--;
}
if(i <= j) {
swap(a, i, j);
i++;
j--;
}
}
swap(a, j, left);
return j;
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
int [] array = {6, 5, 1, 2, 4};
System.out.println(quick(array, 0, array.length - 1, 0)); // 1
System.out.println(quick(array, 0, array.length - 1, 1)); // 2
System.out.println(quick(array, 0, array.length - 1, 2)); // 4
System.out.println(quick(array, 0, array.length - 1, 3)); // 3
System.out.println(quick(array, 0, array.length - 1, 4)); // 6
}
}
1. 数组中第k个最大元素
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 1:
输入: [3,2,1,5,6,4], k = 2 输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4 输出: 4
提示:
1 <= k <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
解法一:执行耗时6ms
class Solution {
public int findKthLargest(int[] nums, int k) {
return quick(nums, 0, nums.length - 1, nums.length - k);
}
public static int quick(int[] array, int left, int right, int i) {
// 基准点元素索引值
int p = partition(array, left, right);
if (p == i) {
return array[p];
}
if (i < p) {
// 到左边找
return quick(array, left, p - 1, i);
} else {
// 到右边找
return quick(array, p + 1, right, i);
}
}
private static int partition(int[] a, int left, int right) {
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
swap(a, idx, left);
int pivot = a[left];
int i = left + 1, j = right;
while (i <= j) {
// 2. i从左向右找大的
while (i <= j && a[i] < pivot) {
i++;
}
// 1. j从右向左找小(等)的
while (i <= j && a[j] > pivot) {
j--;
}
if(i <= j) {
swap(a, i, j);
i++;
j--;
}
}
swap(a, j, left);
return j;
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
2. 数组中位数
package com.itheima.algorithms.divideandconquer;
import java.util.concurrent.ThreadLocalRandom;
class Solution {
/*
偶数个
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, 0, nums.length - 1, nums.length / 2);
} else {
int a = findIndex(nums, 0, nums.length - 1, nums.length / 2);
int b = findIndex(nums, 0, nums.length - 1, nums.length / 2 - 1);
return (a + b) / 2.0;
}
}
public static int findIndex(int[] array, int left, int right, int i) {
/*
6 5 1 2 [4]
2
1 2 4 6 5
1 2 4 6 [5]
3
1 2 4 5 6
*/
// 基准点元素索引值
int p = partition(array, left, right);
if (p == i) {
return array[p];
}
if (i < p) {
// 到左边找
return findIndex(array, left, p - 1, i);
} else {
// 到右边找
return findIndex(array, p + 1, right, i);
}
}
private static int partition(int[] a, int left, int right) {
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
swap(a, idx, left);
int pivot = a[left];
int i = left + 1, j = right;
while (i <= j) {
// 2. i从左向右找大的
while (i <= j && a[i] < pivot) {
i++;
}
// 1. j从右向左找小(等)的
while (i <= j && a[j] > pivot) {
j--;
}
if (i <= j) {
swap(a, i, j);
i++;
j--;
}
}
swap(a, j, left);
return j;
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
int[] nums = {1, 4, 5, 2, 7};
System.out.println(findMedian(nums));
int[] nums2 = {3, 2, 8, 5, 7, 10};
System.out.println(findMedian(nums2));
}
}
三、快速幂
实现 pow(x, n) ,即计算 x
的整数 n
次幂函数(即,xn
)。
示例 1:
输入:x = 2.00000, n = 10 输出:1024.00000
示例 2:
输入:x = 2.10000, n = 3 输出:9.26100
示例 3:
输入:x = 2.00000, n = -2 输出:0.25000 解释:2-2 = 1/22 = 1/4 = 0.25
提示:
-100.0 < x < 100.0
-2^31 <= n <= 2^31-1
n
是一个整数- 要么
x
不为零,要么n > 0
。 -10^4 <= xn <= 10^4
解法一:
class Solution {
public static double myPow(double x, int n) {
if(n == 0) {
return 1.0;
}
if(n == 1) {
return x;
}
double y = myPow(x, n / 2);
/*
1 001
3 011
5 101
7 111
001 &
---
001
2 010
4 100
6 110
8 1000
奇数的二进制末位都是1,偶数的二进制末位都是0
与001进行按位与运算,如果结果为0,则为偶数
*/
if((n & 1) == 0) {
// 偶数次幂
return y * y;
} else if(n > 0){
// 奇数
return x * y * y;
} else {
// n为负数
return y * y / x;
}
}
}
四、平方根整数部分
给你一个非负整数 x
,计算并返回 x
的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5)
或者 x ** 0.5
。
示例 1:
输入:x = 4 输出:2
示例 2:
输入:x = 8 输出:2 解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
提示:
0 <= x <= 2^31 - 1
解法一:
class Solution {
public int mySqrt(int x) {
if(x == 1) {
return 1;
}
int min = 0;
int max = x;
while(max - min > 1) {
int mid = (max + min) >> 1;
if(x / mid < mid) {
// 平方根落在m的左侧,更新max
max = mid;
} else {
// 平方根落在m的右侧,更新min
min = mid;
}
}
return min;
}
}
五、至少k个重复字符的最长子串
给你一个字符串 s
和一个整数 k
,请你找出 s
中的最长子串, 要求该子串中的每一字符出现次数都不少于 k
。返回这一子串的长度。
如果不存在这样的子字符串,则返回 0。
示例 1:
输入:s = "aaabb", k = 3 输出:3 解释:最长子串为 "aaa" ,其中 'a' 重复了 3 次。
示例 2:
输入:s = "ababbc", k = 2 输出:5 解释:最长子串为 "ababb" ,其中 'a' 重复了 2 次, 'b' 重复了 3 次。
提示:
1 <= s.length <= 10^4
s
仅由小写英文字母组成1 <= k <= 10^5
解法一:分治
class Solution {
public int longestSubstring(String s, int k) {
return longestSubstringHelper(s, k);
}
private int longestSubstringHelper(String s, int k) {
HashMap<Character, Integer> map = new HashMap<>();
// 统计字符出现次数
for (char ch : s.toCharArray()) {
map.put(ch, map.getOrDefault(ch, 0) + 1);
}
// 检查是否所有字符都满足出现次数要求
for (char ch : map.keySet()) {
if (map.get(ch) < k) {
// 如果某个字符出现次数小于 k,则分割字符串
int maxLen = 0;
for (String part : s.split(String.valueOf(ch))) {
maxLen = Math.max(maxLen, longestSubstringHelper(part, k));
}
return maxLen;
}
}
// 所有字符出现次数都大于等于 k,返回当前字符串的长度
return s.length();
}
}
或
class Solution {
public int longestSubstring(String s, int k) {
if (s.length() < k) {
return 0;
}
int[] counts = new int[26];
char[] chars = s.toCharArray();
for (char ch : chars) {
counts[ch - 'a']++;
}
System.out.println(Arrays.toString(counts));
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
int count = counts[c - 'a'];
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();
}
}
优化:
class Solution {
public int longestSubstring(String s, int k) {
// 基础判定,字符串长度小于k时返回0
if (s.length() < k) {
return 0;
}
// 统计每个字符出现的次数
int[] counts = new int[26];
for (char ch : s.toCharArray()) {
counts[ch - 'a']++;
}
// 检查是否有字符出现次数小于k
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (counts[c - 'a'] < k) {
// 统计到出现次数小于k的字符,进行分治
int j = i + 1;
while (j < s.length() && counts[s.charAt(j) - 'a'] < k) {
j++;
}
// 递归处理左右部分的子字符串
return Math.max(longestSubstring(s.substring(0, i), k),
longestSubstring(s.substring(j), k));
}
}
// 所有字符的出现次数都大于等于k,返回原字符串的长度
return s.length();
}
}