左程云算法基础第二章:认识O(NlogN)的排序
-
在二分查找等需要把数组进行二等分的操作中,取中点可以采用以下写法
mid = left + ((right - left) >> 1)
在正常情况下的写法
mid = (left + right) / 2
,但如果范围太大可能会导致溢出,那么换成right - left
就不会溢出。 -
递归函数的递归过程类似一个二叉树的后序遍历
归并排序:
/**
* @Description:
* 1.把长度为n的输入序列分成两个长度为n/2的子序列;
* 2. 对这两个子序列分别采用归并排序;
* 3. 将两个排序好的子序列合并成一个最终的排序序列。
*
* 平均时间复杂度:O(NlgN)
* 最优时间复杂度:O(NlgN)
* 最差时间复杂度:O(NlgN)
* 空间复杂度:O(n)
* 稳定性:稳定
* @Author: chong
* @Data: 2021/5/28 10:31 上午
*/
public class MergeSortTemplate {
// 创建一个辅助数组
private int[] aux;
public void mergeSort(int[] nums){
// 为辅助数组分配空间
aux = new int[nums.length];
int low = 0;
int high = nums.length - 1;
mergeSort(nums, low, high);
}
public void mergeSort(int[] nums, int low, int high){
// 如果子数组没有元素或只有一个元素了就返回
if (low >= high)
return;
// 找到子数组的中点
int middle = low + ((high - low) >> 1);
// 对于左右子数组进行递归
mergeSort(nums, low, middle);
mergeSort(nums, middle + 1, high);
// 递归结束之后从最小的数组开始聚合(最小有两个元素)
merge(nums, low, middle, high);
}
/**
* 对子数组进行聚合,实际的排序在此发生,对于每个小数组进行小数组范围内的排序,每次保证输入的两个子数组合并且变为有序的
* @param nums
* @param low:左子数组的开始下标
* @param middle:左右子数组的中点,middle下标处的元素属于左子数组
* @param high:右子数组的结束下标
*/
public void merge(int[] nums, int low, int middle, int high){
// 这一步是记录下左子数组和右子数组的开始下标,记为left,right
int left = low;
int right = middle + 1;
// 要把左右子数组所涉及的元素保存在辅助数组中
for (int k = low; k <= high; k++){
aux[k] = nums[k];
}
// 由于辅助数组已经保存下来了原来的值,故可以直接在原数组nums上做改动
// high是子数组最大元素下标,也要遍历到
for (int h = low; h <= high; h++){
// left>middle时说明左子数组已经添加完了,所以添加右子数组的元素right++
if (left > middle)
nums[h] = aux[right++];
// right>high说明右子数组添加完了,添加左子数组元素left++
else if (right > high)
nums[h] = aux[left++];
// 比较左右子数组指针所指位置元素大小,左子数组(已排序)要比右子数组(已排序)小,
// 所以如果aux[left] > aux[right] 就把小的元素aux[right]添加进去(从小到大的顺序),反之相反。
// 使用辅助数组进行比较,原数组在排序过程中会改变
else if (aux[left] > aux[right])
nums[h] = aux[right++];
else
nums[h] = aux[left++];
}
}
}
-
归并排序的master公式:T(N) = 2T(N/2) + O(N)
-
归并排序相关题目应用
小和问题在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。例子:[1,3,4,2,5]1左边比1小的数,没有;3左边比3小的数,1;4左边比4小的数,1、3;2左边比2小的数,1;5左边比5小的数,1、3、4、2;所以小和为1+1+3+1+1+3+4+2=16
换个思路,从第一个数开始,看右面有多少个数比他大,就有多少个这个数的小和,如上面的数组中1的右边有四个数比它大,则产生4个1的小和,3右边有两个数比他大,产生2个3的小和,依次求解。
这个思路下可以用归并排序的思想解决,先将数组不断分到最小,然后开始merge,在merge的过程中,如果左子数组中的元素比右子数组的元素小,那么就产生小和,产生了n个左子数组当前元素的小和,这个n可以通过右子数组当前元素的下标知道右子数组中还有几个比左子数组当前元素大的元素。
代码:
/**
* @Description:
* 归并排序思想的应用:
* 小和问题在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
* 例子:[1,3,4,2,5]1左边比1小的数,没有;3左边比3小的数,1;4左边比4小的数,1、3;2左边比2小的数,1;
* 5左边比5小的数,1、3、4、2;所以小和为1+1+3+1+1+3+4+2=16
* @Author: chong
* @Data: 2021/6/19 8:04 下午
*/
public class LittleSum {
int[] aux;
int result;
public int littleSum(int[] nums){
aux = new int[nums.length];
result = 0;
littleSum(nums, 0, nums.length - 1);
return result;
}
private void littleSum(int[] nums, int low, int high){
if (low == high)
return;
int mid = low + ((high - low) >> 1);
littleSum(nums, low, mid);
littleSum(nums, mid + 1, high);
merge(nums, low, mid, high);
}
private void merge(int[] nums, int low, int mid, int high){
int left = low;
int right = mid + 1;
for (int i = low; i <= high; i++)
aux[i] = nums[i];
for (int i = low; i <= high; i++){
if (left > mid)
nums[i] = aux[right++];
else if (right > high)
nums[i] = aux[left++];
else if (aux[left] < aux[right]){
result += aux[left] * (high - right + 1);
nums[i] = aux[left++];
// 这里注意当两个指针位置上的元素相等时,一定先拷贝右子数组,才能知道左子数组多少元素比右子数组小
}else
nums[i] = aux[right++];
}
}
@Test
public void test(){
int[] nums = {1, 1, 5, 1, 1, 3};
int res = littleSum(nums);
System.out.println(res);
}
}
逆序对问题,在一个数组中,如果左边的数字比右边的数字大,则这两个数字构成一个逆序对,请打印所有逆序对
leetcode hard题:[剑指 Offer 51. 数组中的逆序对]
/**
* @Description:
* 归并应用:逆序对问题,在一个数组中,如果左边的数字比右边的数字大,则这两个数字构成一个逆序对,请打印所有逆序对,求出总数
* @Author: chong
* @Data: 2021/6/20 2:27 下午
*/
public class reversePairs {
int[] aux;
int res;
public int reversePairs(int[] nums) {
aux = new int[nums.length];
res = 0;
reversePairs(nums, 0, nums.length - 1);
return res;
}
public void reversePairs(int[] nums, int low, int high){
if (low >= high)
return;
int mid = low + ((high - low) >> 1);
reversePairs(nums, low, mid);
reversePairs(nums, mid + 1, high);
merge(nums, low, mid, high);
}
public void merge(int[] nums, int low, int mid, int high){
int left = low;
int right = mid + 1;
for (int i = low; i <= high; i++){
aux[i] = nums[i];
}
for (int i = low; i <= high; i++){
if (left > mid)
nums[i] = aux[right++];
else if (right > high)
nums[i] = aux[left++];
else if (aux[left] > aux[right]){
res += mid - left + 1;
for (int k = left; k <= mid; k++)
System.out.println("逆序对:[" + aux[k] + ", " + aux[right] + "]");
nums[i] = aux[right++];
}else
nums[i] = aux[left++];
}
}
@Test
public void test(){
int[] nums = {7, 5, 6, 4};
int res = reversePairs(nums);
System.out.println(res);
}
}
快速排序
给定一个数组arr,和一个数num,请把小于等于num的数放在数 组的左边,大于num的数放在数组的右边。
要求额外空间复杂度O(1),时间复杂度O(N)
设定好num,用i遍历数组,当i比num小于等于的时候进入情况一:将i与小于等于区域的下一位交换,小于等于区域向右扩一位,i++。当i比num大的时候进入情况二:i++
- 荷兰国旗问题
给定一个数组arr,和一个数num,请把小于num的数放在数组的 左边,等于num的数放在数组的中间,大于num的数放在数组的 右边。
要求额外空间复杂度O(1),时间复杂度O(N)
这道题与leetcode上75. 颜色分类相同,实际上利用了双指针指明大于和小于区域,需要注意怎么变化。
本题题解可见【LeetCode刷题笔记】75.颜色分类
同样分为三种情况:
- i<num:i和小于区域下一个做交换,小于区域右扩,i++
- i=num:i++
- i>num:i和大于区域前一个交换,大于区域左扩,i原地不变
快速排序1.0
用数组最后一个数字做划分(或者第一个),把整个数组划分为小于等于最后一个数字的和大于最后一个数字的
快速排序2.0
用数组最后一个数字做划分,把整个数组划分为小于最后一个数字的和等于最后一个数字的和大于最后一个数字的,比1.0版本稍快
最差时间复杂度:O(N^2)
最差的时候是数组已经是排好序的时候
产生的原因是划分值打得很偏
快速排序3.0
在数组中随机选一个数,放在最后一个位置上,再拿它做划分值(随机选保证好坏情况等概率发生)