归并排序和快速排序
2021-05-02 10:27
标签:随机 ram 学习 额外 好的 等于 有序数组 操作 空间复杂度 1、归并排序 基本思路:借助额外空间,合并两个有序数组,得到更长的有序数组。例如:「力扣」第 88 题:合并两个有序数组。 算法思想:分而治之(分治思想)。「分而治之」思想的形象理解是「曹冲称象」、MapReduce,在一定情况下可以并行化。 优化 1:在「小区间」里转向使用「插入排序」,Java 源码里面也有类似这种操作,「小区间」的长度是个超参数,需要测试决定,我这里参考了 JDK 源码; 优化 2: 在「两个数组」本身就是有序的情况下,无需合并; 优化 3:全程使用一份临时数组进行「合并两个有序数组」的操作,避免创建临时数组和销毁的消耗,避免计算下标偏移量。 注意:实现归并排序的时候,要特别注意,不要把这个算法实现成非稳定排序,区别就在
「归并排序」比「快速排序」好的一点是,它借助了额外空间,可以实现「稳定排序」,Java 里对于「对象数组」的排序任务,就是使用归并排序(的升级版 TimSort,在这里就不多做介绍)。 复杂度分析: 时间复杂度:O(N \log N)O(NlogN),这里 NN 是数组的长度; 经典问题: 《剑指 Offer》第 51 题:数组中的逆序对,照着归并排序的思路就能写出来。 未优化版归并排序: 2、快速排序 基本思路:快速排序每一次都排定一个元素(这个元素呆在了它最终应该呆的位置),然后递归地去排它左边的部分和右边的部分,依次进行下去,直到数组有序; 算法思想:分而治之(分治思想),与「归并排序」不同,「快速排序」在「分」这件事情上不想「归并排序」无脑地一分为二,而是采用了 partition 的方法(书上,和网上都有介绍,就不展开了),因此就没有「合」的过程。 实现细节(注意事项):(针对特殊测试用例:顺序数组或者逆序数组)一定要随机化选择切分元素(pivot),否则在输入数组是有序数组或者是逆序数组的时候,快速排序会变得非常慢(等同于冒泡排序或者「选择排序」); 以下是针对特殊测试用例(有很多重复元素的输入数组)有 3 种版本的***: 版本 1:基本***:把等于切分元素的所有元素分到了数组的同一侧,可能会造成递归树倾斜; 写对「快速排序」的技巧:保持「循环不变量」,即定义的变量在循环开始前、循环过程中、循环结束以后,都保持不变的性质,这个性质是人为根据问题特点定义的。 使用「快速排序」解决的经典问题(非常重要): TopK 问题:「力扣」第 215 题:数组中的第 K 个最大元素; 版本1: 版本2: 版本3: 参考文章:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/ 归并排序和快速排序 标签:随机 ram 学习 额外 好的 等于 有序数组 操作 空间复杂度 原文地址:https://www.cnblogs.com/jing-yi/p/13204160.htmlpublic class Solution {
// 归并排序
/**
* 列表大小等于或小于该大小,将优先于 mergeSort 使用插入排序
*/
private static final int INSERTION_SORT_THRESHOLD = 7;
public int[] sortArray(int[] nums) {
int len = nums.length;
int[] temp = new int[len];
mergeSort(nums, 0, len - 1, temp);
return nums;
}
/**
* 对数组 nums 的子区间 [left, right] 进行归并排序
*
* @param nums
* @param left
* @param right
* @param temp 用于合并两个有序数组的辅助数组,全局使用一份,避免多次创建和销毁
*/
private void mergeSort(int[] nums, int left, int right, int[] temp) {
// 小区间使用插入排序
if (right - left INSERTION_SORT_THRESHOLD) {
insertionSort(nums, left, right);
return;
}
int mid = left + (right - left) / 2;
// Java 里有更优的写法,在 left 和 right 都是大整数时,即使溢出,结论依然正确
// int mid = (left + right) >>> 1;
mergeSort(nums, left, mid, temp);
mergeSort(nums, mid + 1, right, temp);
// 如果数组的这个子区间本身有序,无需合并
if (nums[mid] ]) {
return;
}
mergeOfTwoSortedArray(nums, left, mid, right, temp);
}
/**
* 对数组 arr 的子区间 [left, right] 使用插入排序
*
* @param arr 给定数组
* @param left 左边界,能取到
* @param right 右边界,能取到
*/
private void insertionSort(int[] arr, int left, int right) {
for (int i = left + 1; i ) {
int temp = arr[i];
int j = i;
while (j > left && arr[j - 1] > temp) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = temp;
}
}
/**
* 合并两个有序数组:先把值复制到临时数组,再合并回去
*
* @param nums
* @param left
* @param mid [left, mid] 有序,[mid + 1, right] 有序
* @param right
* @param temp 全局使用的临时数组
*/
private void mergeOfTwoSortedArray(int[] nums, int left, int mid, int right, int[] temp) {
System.arraycopy(nums, left, temp, left, right + 1 - left);
int i = left;
int j = mid + 1;
for (int k = left; k ) {
if (i == mid + 1) {
nums[k] = temp[j];
j++;
} else if (j == right + 1) {
nums[k] = temp[i];
i++;
} else if (temp[i] temp[j]) {
// 注意写成
nums[k] = temp[i];
i++;
} else {
// temp[i] > temp[j]
nums[k] = temp[j];
j++;
}
}
}
}
空间复杂度:O(N)O(N),辅助数组与输入数组规模相当。
「归并排序」也有「原地归并排序」和「不使用递归」的归并排序,但是我个人觉得不常用,编码、调试都有一定难度。递归、分治处理问题的思想在基础算法领域是非常常见的,建议多练习编写「归并排序」学习递归思想,了解递归的细节,熟悉分治的思想。
「力扣」第 315 题:计算右侧小于当前元素的个数,它们是一个问题。public static void main(String[] args) {
int[] arrays = {9, 2, 5, 1, 3, 2, 9, 5, 2, 1, 8};
sort(arrays, 0, arrays.length - 1);
for (Integer i : arrays){
System.out.print(i + "-");
}
}
private static void sort(int[] array, int left,int right){
if(left == right){
return;
}
int mid = (left + right) / 2;
sort(array,left,mid);
sort(array,mid+1,right);
merge(array,left,mid+1,right);
}
private static void merge(int[] arrays,int L,int M,int R){
//左边的数组的大小
int[] leftArray = new int[M - L];
//右边的数组大小
int[] rightArray = new int[R - M + 1];
//往这两个数组填充数据
for (int i = L; i ) {
leftArray[i - L] = arrays[i];
}
for (int i = M; i ) {
rightArray[i - M] = arrays[i];
}
int i = 0, j = 0;
// arrays数组的第一个元素
int k = L;
//比较这两个数组的值,哪个小,就往数组上放
while (i rightArray.length) {
//谁比较小,谁将元素放入大数组中,移动指针,继续比较下一个
if (leftArray[i] rightArray[j]) {
arrays[k] = leftArray[i];
i++;
k++;
} else {
arrays[k] = rightArray[j];
j++;
k++;
}
}
//如果左边的数组还没比较完,右边的数都已经完了,那么将左边的数抄到大数组中(剩下的都是大数字)
while (i leftArray.length) {
arrays[k] = leftArray[i];
i++;
k++;
}
//如果右边的数组还没比较完,左边的数都已经完了,那么将右边的数抄到大数组中(剩下的都是大数字)
while (j rightArray.length) {
arrays[k] = rightArray[j];
k++;
j++;
}
}
版本 2:双指针***:把等于切分元素的所有元素等概率地分到了数组的两侧,避免了递归树倾斜,递归树相对平衡;
版本 3:三指针***:把等于切分元素的所有元素挤到了数组的中间,在有很多元素和切分元素相等的情况下,递归区间大大减少。
这里有一个经验的总结:之所以***有这些优化,起因都是来自「递归树」的高度。关于「树」的算法的优化,绝大部分都是在和树的「高度」较劲。类似的通过减少树高度、使得树更平衡的数据结构还有「二叉搜索树」优化成「AVL 树」或者「红黑树」、「并查集」的「按秩合并」与「路径压缩」。
「循环不变量」的内容在《算法导论》这本书里有介绍。我个人觉得非常有用。「循环不变量」是证明算法有效性的基础,更是写对代码的保证,遵守循环不变量,是不是该写等于号,先交换还是先 ++ ,就会特别清楚,绝对不会写错,我在编码的时候,会将遵守的「循环不变量」作为注释写在代码中。
快速排序丢失了稳定性,如果需要稳定的快速排序,需要具体定义比较函数,这个过程叫「稳定化」,在这里就不展开了。
荷兰国旗问题:「力扣」第 75 题:颜色分类。import java.util.Random;
public class Solution {
// 快速排序 1:基本快速排序
/**
* 列表大小等于或小于该大小,将优先于 quickSort 使用插入排序
*/
private static final int INSERTION_SORT_THRESHOLD = 7;
private static final Random RANDOM = new Random();
public int[] sortArray(int[] nums) {
int len = nums.length;
quickSort(nums, 0, len - 1);
return nums;
}
private void quickSort(int[] nums, int left, int right) {
// 小区间使用插入排序
if (right - left INSERTION_SORT_THRESHOLD) {
insertionSort(nums, left, right);
return;
}
int pIndex = partition(nums, left, right);
quickSort(nums, left, pIndex - 1);
quickSort(nums, pIndex + 1, right);
}
/**
* 对数组 nums 的子区间 [left, right] 使用插入排序
*
* @param nums 给定数组
* @param left 左边界,能取到
* @param right 右边界,能取到
*/
private void insertionSort(int[] nums, int left, int right) {
for (int i = left + 1; i ) {
int temp = nums[i];
int j = i;
while (j > left && nums[j - 1] > temp) {
nums[j] = nums[j - 1];
j--;
}
nums[j] = temp;
}
}
private int partition(int[] nums, int left, int right) {
int randomIndex = RANDOM.nextInt(right - left + 1) + left;
swap(nums, left, randomIndex);
// 基准值
int pivot = nums[left];
int lt = left;
// 循环不变量:
// all in [left + 1, lt] // all in [lt + 1, i) >= pivot
for (int i = left + 1; i ) {
if (nums[i] pivot) {
lt++;
swap(nums, i, lt);
}
}
swap(nums, left, lt);
return lt;
}
private void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
}
import java.util.Random;
public class Solution {
// 快速排序 2:双指针(指针对撞)快速排序
/**
* 列表大小等于或小于该大小,将优先于 quickSort 使用插入排序
*/
private static final int INSERTION_SORT_THRESHOLD = 7;
private static final Random RANDOM = new Random();
public int[] sortArray(int[] nums) {
int len = nums.length;
quickSort(nums, 0, len - 1);
return nums;
}
private void quickSort(int[] nums, int left, int right) {
// 小区间使用插入排序
if (right - left INSERTION_SORT_THRESHOLD) {
insertionSort(nums, left, right);
return;
}
int pIndex = partition(nums, left, right);
quickSort(nums, left, pIndex - 1);
quickSort(nums, pIndex + 1, right);
}
/**
* 对数组 nums 的子区间 [left, right] 使用插入排序
*
* @param nums 给定数组
* @param left 左边界,能取到
* @param right 右边界,能取到
*/
private void insertionSort(int[] nums, int left, int right) {
for (int i = left + 1; i ) {
int temp = nums[i];
int j = i;
while (j > left && nums[j - 1] > temp) {
nums[j] = nums[j - 1];
j--;
}
nums[j] = temp;
}
}
private int partition(int[] nums, int left, int right) {
int randomIndex = left + RANDOM.nextInt(right - left + 1);
swap(nums, randomIndex, left);
int pivot = nums[left];
int lt = left + 1;
int gt = right;
// 循环不变量:
// all in [left + 1, lt) // all in (gt, right] >= pivot
while (true) {
while (lt pivot) {
lt++;
}
while (gt > left && nums[gt] > pivot) {
gt--;
}
if (lt >= gt) {
break;
}
// 细节:相等的元素通过交换,等概率分到数组的两边
swap(nums, lt, gt);
lt++;
gt--;
}
swap(nums, left, gt);
return gt;
}
private void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
}
import java.util.Random;
public class Solution {
// 快速排序 3:三指针快速排序
/**
* 列表大小等于或小于该大小,将优先于 quickSort 使用插入排序
*/
private static final int INSERTION_SORT_THRESHOLD = 7;
private static final Random RANDOM = new Random();
public int[] sortArray(int[] nums) {
int len = nums.length;
quickSort(nums, 0, len - 1);
return nums;
}
private void quickSort(int[] nums, int left, int right) {
// 小区间使用插入排序
if (right - left INSERTION_SORT_THRESHOLD) {
insertionSort(nums, left, right);
return;
}
int randomIndex = left + RANDOM.nextInt(right - left + 1);
swap(nums, randomIndex, left);
// 循环不变量:
// all in [left + 1, lt] // all in [lt + 1, i) = pivot
// all in [gt, right] > pivot
int pivot = nums[left];
int lt = left;
int gt = right + 1;
int i = left + 1;
while (i gt) {
if (nums[i] pivot) {
lt++;
swap(nums, i, lt);
i++;
} else if (nums[i] == pivot) {
i++;
} else {
gt--;
swap(nums, i, gt);
}
}
swap(nums, left, lt);
// 注意这里,大大减少了两侧分治的区间
quickSort(nums, left, lt - 1);
quickSort(nums, gt, right);
}
/**
* 对数组 nums 的子区间 [left, right] 使用插入排序
*
* @param nums 给定数组
* @param left 左边界,能取到
* @param right 右边界,能取到
*/
private void insertionSort(int[] nums, int left, int right) {
for (int i = left + 1; i ) {
int temp = nums[i];
int j = i;
while (j > left && nums[j - 1] > temp) {
nums[j] = nums[j - 1];
j--;
}
nums[j] = temp;
}
}
private void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
}
上一篇:209. 长度最小的子数组
下一篇:快速排序