跳到主要内容Java 分治算法实战:快速排序与归并排序 | 极客日志Javajava算法
Java 分治算法实战:快速排序与归并排序
基于 Java 语言,深入探讨分治算法在快速排序、归并排序及相关变体问题中的应用。涵盖颜色分类、数组排序、第 K 大元素、逆序对计数及右侧小于当前元素个数等经典场景。重点解析三路快排优化、随机基准值选取、归并排序中的统计技巧以及索引映射处理,提供完整可运行的代码示例与复杂度分析。
Java 分治算法实战
分治思想是算法设计中的核心范式之一,通过递归地将大问题拆解为小问题来解决。在排序领域,快速排序和归并排序是最经典的两个应用。本文将结合 LeetCode 上的经典题目,深入剖析这两种算法的实现细节、优化策略以及变体应用。
1. 颜色分类
题目链接: 颜色分类

这道题要求将数组中的 0、1、2 分别排在左侧、中间和右侧。这其实是荷兰国旗问题的变种。我们可以借鉴双指针中'移动零'的思路,将数组划分为三个区域:
[0, left]:全是 0
[left+1, i-1]:全是 1
[i, right-1]:待扫描区域
[right, n-1]:全是 2
使用 left、i、right 三个指针进行遍历。当遇到 0 时,交换到左侧;遇到 2 时,交换到右侧(注意此时 right 指向的元素未扫描,不能移动 i);遇到 1 则直接跳过。
class Solution {
public void sortColors(int[] nums) {
int left = -1;
int i = 0;
int right = nums.length;
while (i < right) {
if (nums[i] == 0) {
swap(nums, ++left, i++);
} else if (nums[i] == 1) {
i++;
} else {
swap(nums, --right, i);
}
}
}
{
nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
private
void
swap
(int[] nums, int i, int j)
int
temp
=
2. 排序数组(快速排序)
快速排序的核心在于选取基准值(pivot),将数组分为小于 pivot、等于 pivot、大于 pivot 三部分,然后递归处理左右两侧。为了避免在最坏情况(如已排序数组)下退化为 O(n²),我们通常采用随机选取基准值的策略。
class Solution {
public int[] sortArray(int[] nums) {
qsort(nums, 0, nums.length - 1);
return nums;
}
private void qsort(int[] nums, int l, int r) {
if (l >= r) {
return;
}
int key = nums[new Random().nextInt(r - l + 1) + l];
int left = l - 1;
int right = r + 1;
int i = l;
while (i < right) {
if (nums[i] < key) {
swap(nums, ++left, i++);
} else if (nums[i] == key) {
i++;
} else {
swap(nums, --right, i);
}
}
qsort(nums, l, left);
qsort(nums, right, r);
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
3. 数组中第 K 个最大元素
如果完全排序再取第 K 个,时间复杂度是 O(n log n)。利用快速选择的思想,我们只需要找到第 K 大的位置即可。根据三路快排划分的结果,判断第 K 大元素落在哪个区间,只递归进入该区间,平均时间复杂度可降至 O(n)。
class Solution {
public int findKthLargest(int[] nums, int k) {
return qsort(nums, 0, nums.length - 1, k);
}
private int qsort(int[] nums, int l, int r, int k) {
if (l == r) {
return nums[l];
}
int key = nums[new Random().nextInt(r - l + 1) + l];
int left = l - 1;
int right = r + 1;
int i = l;
while (i < right) {
if (nums[i] < key) {
swap(nums, ++left, i++);
} else if (nums[i] == key) {
i++;
} else {
swap(nums, --right, i);
}
}
int b = right - left - 1;
int c = r - right + 1;
if (c >= k) {
return qsort(nums, right, r, k);
} else if ((b + c) >= k) {
return key;
} else {
return qsort(nums, l, left, k - b - c);
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
4. 最小的 K 个数
思路与上一题类似,只是方向相反。我们需要找到最小的 K 个数,同样利用快速选择的分区特性,只处理包含最小元素的区间。
class Solution {
public int[] smallestK(int[] arr, int k) {
qsort(arr, 0, arr.length - 1, k);
int[] ret = new int[k];
for (int i = 0; i < k; i++) {
ret[i] = arr[i];
}
return ret;
}
private void qsort(int[] arr, int l, int r, int k) {
if (l >= r) {
return;
}
int key = arr[new Random().nextInt(r - l + 1) + l];
int left = l - 1;
int right = r + 1;
int i = l;
while (i < right) {
if (arr[i] < key) {
swap(arr, ++left, i++);
} else if (arr[i] == key) {
i++;
} else {
swap(arr, --right, i);
}
}
int a = left - l + 1;
int b = right - left - 1;
if (a > k) {
qsort(arr, l, left, k);
} else if (a + b >= k) {
return;
} else {
qsort(arr, right, r, k - a - b);
}
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
5. 排序数组(归并排序)
归并排序是稳定排序的代表,其核心在于'分而治之'。先递归地将数组一分为二直到不可分,然后在合并过程中保证有序性。相比快速排序,归并排序的空间复杂度较高,但性能更稳定。
class Solution {
int[] tem;
public int[] sortArray(int[] nums) {
tem = new int[nums.length];
mergeSort(nums, 0, nums.length - 1);
return nums;
}
private void mergeSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
int mid = (left + right) / 2;
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
int cur1 = left;
int cur2 = mid + 1;
int i = 0;
while (cur1 <= mid && cur2 <= right) {
tem[i++] = nums[cur1] >= nums[cur2] ? nums[cur2++] : nums[cur1++];
}
while (cur1 <= mid) {
tem[i++] = nums[cur1++];
}
while (cur2 <= right) {
tem[i++] = nums[cur2++];
}
for (int j = left; j <= right; j++) {
nums[j] = tem[j - left];
}
}
}
6. 交易逆序对的总数
逆序对是指满足 i < j 且 nums[i] > nums[j] 的数对。在归并排序的合并阶段,当右半部分的元素被选中放入临时数组时,说明它比左半部分当前剩余的所有元素都小,因此可以一次性统计出多个逆序对。
class Solution {
int[] tem;
public int reversePairs(int[] record) {
tem = new int[record.length];
return mergeSort(record, 0, record.length - 1);
}
private int mergeSort(int[] record, int left, int right) {
if (left >= right) {
return 0;
}
int mid = (left + right) / 2;
int ret = mergeSort(record, left, mid) + mergeSort(record, mid + 1, right);
int cur1 = left;
int cur2 = mid + 1;
int i = 0;
while (cur1 <= mid && cur2 <= right) {
if (record[cur1] <= record[cur2]) {
tem[i++] = record[cur1++];
} else {
ret += mid - cur1 + 1;
tem[i++] = record[cur2++];
}
}
while (cur1 <= mid) {
tem[i++] = record[cur1++];
}
while (cur2 <= right) {
tem[i++] = record[cur2++];
}
for (int j = left; j <= right; j++) {
record[j] = tem[j - left];
}
return ret;
}
}
7. 翻转对
此题要求找出满足 i < j 且 nums[i] > 2 * nums[j] 的对数。由于乘法可能溢出,建议改为除法比较 nums[i] / 2.0 <= nums[j]。逻辑上依然是在归并排序的合并前或合并过程中统计。
class Solution {
int[] tem;
public int reversePairs(int[] nums) {
int n = nums.length;
tem = new int[n];
return mergeSort(nums, 0, n - 1);
}
private int mergeSort(int[] nums, int left, int right) {
if (left >= right) {
return 0;
}
int ret = 0;
int mid = (left + right) / 2;
ret += mergeSort(nums, left, mid);
ret += mergeSort(nums, mid + 1, right);
int cur1 = left;
int cur2 = mid + 1;
while (cur1 <= mid) {
while (cur2 <= right && nums[cur1] / 2.0 <= nums[cur2]) {
cur2++;
}
if (cur2 > right) {
break;
}
ret += right - cur2 + 1;
cur1++;
}
cur1 = left;
cur2 = mid + 1;
int i = left;
while (cur1 <= mid && cur2 <= right) {
tem[i++] = nums[cur1] >= nums[cur2] ? nums[cur1++] : nums[cur2++];
}
while (cur1 <= mid) {
tem[i++] = nums[cur1++];
}
while (cur2 <= right) {
tem[i++] = nums[cur2++];
}
for (int j = left; j <= right; j++) {
nums[j] = tem[j];
}
return ret;
}
}
8. 计算右侧小于当前元素的个数
归并排序会打乱原始顺序,导致无法直接对应回原下标。解决方案是引入一个索引数组 index,在排序过程中同步移动索引。这样当我们在合并阶段统计逆序对时,就能准确知道是哪个原始位置的元素产生了计数。
class Solution {
int[] ret;
int[] index;
int[] temIndex;
int[] temNums;
public List<Integer> countSmaller(int[] nums) {
int n = nums.length;
ret = new int[n];
index = new int[n];
temIndex = new int[n];
temNums = new int[n];
for (int i = 0; i < n; i++) {
index[i] = i;
}
mergeSort(nums, 0, n - 1);
List<Integer> list = new ArrayList<>();
for (int x : ret) {
list.add(x);
}
return list;
}
private void mergeSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
int mid = (left + right) / 2;
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
int cur1 = left;
int cur2 = mid + 1;
int i = 0;
while (cur1 <= mid && cur2 <= right) {
if (nums[cur1] <= nums[cur2]) {
temNums[i] = nums[cur2];
temIndex[i++] = index[cur2++];
} else {
ret[index[cur1]] += right - cur2 + 1;
temNums[i] = nums[cur1];
temIndex[i++] = index[cur1++];
}
}
while (cur1 <= mid) {
temNums[i] = nums[cur1];
temIndex[i++] = index[cur1++];
}
while (cur2 <= right) {
temNums[i] = nums[cur2];
temIndex[i++] = index[cur2++];
}
for (int j = left; j <= right; j++) {
nums[j] = temNums[j - left];
index[j] = temIndex[j - left];
}
}
}
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online