跳到主要内容Java 分治算法实战:快速排序与归并排序 | 极客日志Javajava算法
Java 分治算法实战:快速排序与归并排序
Java 分治算法实战涵盖颜色分类、数组排序、第 K 大元素、最小 K 个数、逆序对统计及计算右侧小于当前元素个数等问题。核心方案包括三指针法、随机化快速排序、归并排序及其变体。解析了各算法的时间复杂度 O(n) 或 O(nlogn),提供了完整的 Java 代码实现与逻辑推导,适用于算法面试准备与性能优化场景。
CloudNative0 浏览 分治算法
使用快速排序和归并排序解决相关问题,这两个算法均基于分治思想。
颜色分类
题目解析:将数组中 0 放在左边,1 放在中间,2 放在右边。
在双指针算法中有一个移动零的题目,就是将所有 0 元素移动到右边,但是非 0 元素相对位置不改变。那题使用双指针将其数组分为三部分,因此这题也可以将其数组分块。
left表示为 0 区域最右侧,i遍历数组,right表示 2 区域最左侧。使用这三个指针将这个数组分为了 4 部分。
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);
}
}
}
public void swap(int[] nums, int i, int j) {
int tem nums[i];
nums[i] = nums[j];
nums[j] = tem;
}
}
相关免费在线工具
- 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
=
排序数组 (快排)
分治:使用快速排序,找一个 key 基准值,根据基准值,让其数组变成三个模块,左边模块<key,中间模块=key,右边模块>key,这时候在对左右两边的模块,分别进行找基准值进行排序,左右两边每一个模块又可以分为这三个部分,就这样不断排序最终结果就变成有序的了。
key 基准值如何取?这里采用随机取,随机其对应区间下标 r = new Random().nextInt(right - left + 1) + left,这样整个下标区间的取值就是 [left, right] 了。
class Solution {
public int[] sortArray(int[] nums) {
qsort(nums, 0, nums.length - 1);
return nums;
}
public 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);
}
public void swap(int[] nums, int i, int j) {
int tem = nums[i];
nums[i] = nums[j];
nums[j] = tem;
}
}
数组中第 K 个最大元素
题目解析:给了一个数组,找出其中第 K 个大的元素。
快速排序:根据上一题的快速排序,这里我们仍然使用快排,但是这里不一样的是,快速排序以后将数组分为三个模块,这里我们可以判断其第 K 个大的元素在那个模块,此时我们根据其每一个模块元素个数进行判断,在一个模块中,只需要在这一个模块中找即可,剩下的两个模块就不用管了。
class Solution {
public int findKthLargest(int[] nums, int k) {
return qsort(nums, 0, nums.length - 1, k);
}
public 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);
}
}
public void swap(int[] nums, int i, int j) {
int tem = nums[i];
nums[i] = nums[j];
nums[j] = tem;
}
}
最小的 K 个数
快速选择算法 + 随机获取 key 值:这题和上一题找出第 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;
}
public 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);
}
}
public void swap(int[] arr, int i, int j) {
int tem = arr[i];
arr[i] = arr[j];
arr[j] = tem;
}
}
时间复杂度:O(n),因为随机存取 key,所以渐进 O(n)
空间复杂度:O(k)
排序数组 (归并)
这里仍然是归并的思想,根据 mid 将其数组分为左右两部分,左右两部分分别进行排序,左边部分又可以分为左右两部分,右边同理,当左右两边分完以后,就可以进行合并,左边先合并,右边再合并,最后合并到一起,最终整个数组再进行一次排序即可。
class Solution {
int[] tem;
public int[] sortArray(int[] nums) {
tem = new int[nums.length];
mergeSort(nums, 0, nums.length - 1);
return nums;
}
public 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];
}
}
}
交易逆序对的总数
题目解析:相对顺序不变,找出 ai > aj , i < j,这样逆序对的总个数。
class Solution {
int[] tem;
public int reversePairs(int[] record) {
tem = new int[record.length];
return mergeSort(record, 0, record.length - 1);
}
public 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;
}
}
class Solution {
int[] tem;
public int reversePairs(int[] record) {
tem = new int[record.length];
return mergeSort(record, 0, record.length - 1);
}
public 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[cur2++];
} else {
ret += right - cur2 + 1;
tem[i++] = record[cur1++];
}
}
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;
}
}
翻转对
题目解析:找出前面一个元素大于后面元素 2 倍的总个数。
归并:此题目和上题一样,只不过这里变成了 2 倍,上面我们将更新结果和合并数组放到一起,这里我们因为条件不同,所以要将其分开写即可。
这里 2 倍可能会溢出,因此直接让前一个数 / 2.0 进行比较即可。
class Solution {
int[] tem;
public int reversePairs(int[] nums) {
int n = nums.length;
tem = new int[n];
return mergeSort(nums, 0, n - 1);
}
public 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;
}
}
class Solution {
int[] tem;
public int reversePairs(int[] nums) {
int n = nums.length;
tem = new int[n];
return mergeSort(nums, 0, n - 1);
}
public 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 (cur2 <= right) {
while (cur1 <= mid && nums[cur1] / 2.0 <= nums[cur2]) {
cur1++;
}
if (cur1 > mid) {
break;
}
ret += mid - cur1 + 1;
cur2++;
}
cur1 = left;
cur2 = mid + 1;
int i = left;
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];
}
return ret;
}
}
计算右侧小于当前元素的个数
题目解析:返回一个新数组,对应下标的值是后面元素比当前下标元素小的个数。
归并:这一题和上面一题非常类似,但是唯一问题,就是我们不断排序,不断更新结果,然而我们找不到其原本下标进行更新结果,因此这里我们就需要使用一个 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> l = new ArrayList<>();
for (int x : ret) {
l.add(x);
}
return l;
}
public 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];
}
}
}