一、递归
1.1 定义
递归是指将一个(总)问题拆分成更小规模的子问题,通过求解子问题,并将子问题的解合并就能得到(总)问题的解。
例:求解一个数组的最大值。
可以把数组分成两半,分别求解最大值和最小值
class Solution(){
public int max(int[] nums){
return maxRecur(nums, 0, nums.length - 1);
}
private int maxRecur(int[] nums, int i, int j){
if(i == j){return nums[j];}
int mid = (i+j)/2;
int max1 = maxRecur(nums, i, mid);
int max2 = maxRecur(nums, mid+1, j);
return (max1 > max2) ? max1 : max2;
}
}
1.2 递归函数的时间复杂度:master’s theorem
对于上述例子来说,如果规模为n的问题时间复杂度为,那么
将分为三部分:求解左侧最大值,时间复杂度为
;求解右侧最大值,时间复杂度为
;第三步为整合,时间复杂度为
,那么我们得到
这种形如的形式,均可以用master公式求解复杂度。
master公式:(非原版,这里针对多数情况做了简化)
形如
的递推算法,求解时间复杂度时只需要比较
和
:
- 如果
,则
- 如果
,则
- 如果
,则
上例中,a=2,b=2,k=0,满足第一种情况,得到
二、归并排序
2.1 核心思想
将数组排序问题转化为递归问题,对于一个数组,分为三步:
第一步:使数组的左半部分有序
第二步:使数组的右半部分有序
第三步:将两个有序数组合并
我们看到,前两步都是递归的子问题,因此只需要考虑第三步。具体的方式为,使用两个指针,分别指向两个有序数组的第一个元素,哪个元素小,就将哪个元素移到一个新的数组里,并将对应指针右移一位,如果有一个指针已经越过边界,则默认选取另一个指针。直至两个指针都越过边界为止。
2.2 java实现
class Solution {
private int[] temp;
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);
merge(nums, left, mid, right);
}
public void merge(int[] nums, int left, int mid, int right){
int i = left;
int j = mid+1;
int p = left;
while(i <= mid && j <= right){
if(nums[i] <= nums[j]){
temp[p] = nums[i];
++i;
}
else{
temp[p] = nums[j];
++j;
}
++p;
}
while (i <= mid) {
temp[p] = nums[i];
++i;
++p;
}
while (j <= right) {
temp[p] = nums[j];
++j;
++p;
}
for(int k = left; k <= right; k++){
nums[k] = temp[k];
}
}
}
2.3 归并排序的时间负责度
主要分析第三步,合并两个有序数组需要多少次常数操作?对于两个指针来说,无论什么顺序,他们都只会从左向右走,同时一次只走一个,因此所需的时间复杂度为,于是我们得到
,应用master公式情形2,我们得到
。
2.4 归并排序与选择排序
为什么归并排序比选择排序的时间复杂度低?我们回忆一下选择排序,第一步遍历n个数,确认第一个数;第二步遍历n-1个数,确认第二个数;这样下去,问题就在于每一步的比较行为是独立的,第一步比较的很多中间信息,在第二步被浪费了。我们以[2,4,3,1,5]为例,第一步先选出最小值,放到首位,我们得到[1,4,3,2,5]。第二步开始,我们从第二个数开始向后比较,比较4和3,但实际上这两个数在第一步已经比较过了,这就属于比较信息的浪费,也可以理解为重复/无效比较。
我们再看归并排序,以[1,4,3,2,5]为例。对于数字3来说,当第一步中,3和1、4完成了比较,经过第二步,数组变成[1,3,4]和[2,5],而在第三步合并时,3无需再和1、4做比较了,只需要和右边的数比较,这就大大减少了比较信息的浪费,前两步的比较信息在第三步被有效的利用了起来。因此归并排序的时间复杂度优于选择排序。
2.5 leetcode第315题
https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self/
给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
示例 1:
输入:nums = [5,2,6,1]
输出:[2,1,1,0]
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素
分析:本题考虑归并排序,考虑数组[5,2,6,1],,在归并排序的前两步,我们得到[2,5]和[1,6],那么在2的右侧,比它小的数,只可能有两种,一种是在[2,5]里面的数,另一种是在在[1,6]中找到最后一个比2小的数,它左边的数均满足要求。所以我们只需要在归并排序的第三步里顺便把对应的数更新。
class Solution {
private int[] temp;
private int[] tempIndex;
private int[] index;
private int[] res;
public List<Integer> countSmaller(int[] nums) {
this.temp = new int[nums.length];
this.tempIndex = new int[nums.length];
this.index = new int[nums.length];
this.res = new int[nums.length];
for (int i = 0; i < nums.length; ++i) {
index[i] = i;
}
mergeSort(nums, 0, nums.length-1);
List<Integer> ans = new ArrayList<>();
for(int num: res){
ans.add(num);
}
return ans;
}
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);
merge(nums, left, mid, right);
}
public void merge(int[] nums, int left, int mid, int right){
int i = left;
int j = mid+1;
int p = left;
while(i <= mid && j <= right){
if(nums[i] <= nums[j]){
temp[p] = nums[i];
tempIndex[p] = index[i];
res[index[i]] += (j - mid - 1);
++i;
}
else{
temp[p] = nums[j];
tempIndex[p] = index[j];
++j;
}
++p;
}
while (i <= mid) {
temp[p] = nums[i];
tempIndex[p] = index[i];
res[index[i]] += (j - mid - 1);
++i;
++p;
}
while (j <= right) {
temp[p] = nums[j];
tempIndex[p] = index[j];
++j;
++p;
}
for(int k = left; k <= right; k++){
nums[k] = temp[k];
index[k] = tempIndex[k];
}
}
}
2.6 leetcode剑指offer第51题
https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例 1:
输入: [7,5,6,4]
输出: 5
分析:同样是在归并排序的第三步中,顺便更新一下逆序对的数量
class Solution {
private int[] temp;
private int res;
public int reversePairs(int[] nums) {
this.temp = new int[nums.length];
this.res = 0;
mergeSort(nums, 0, nums.length-1);
return this.res;
}
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);
merge(nums, left, mid, right);
}
public void merge(int[] nums, int left, int mid, int right){
int i = left;
int j = mid+1;
int p = left;
while(i <= mid && j <= right){
if(nums[i] <= nums[j]){
temp[p] = nums[i];
res += (j - mid - 1);
++i;
}
else{
temp[p] = nums[j];
++j;
}
++p;
}
while (i <= mid) {
temp[p] = nums[i];
res += (j - mid - 1);
++i;
++p;
}
while (j <= right) {
temp[p] = nums[j];
++j;
++p;
}
for(int k = left; k <= right; k++){
nums[k] = temp[k];
}
}
}
3 快速排序
在介绍快速排序之前,我们先看一下荷兰国旗问题
3.1 leetcode第75题
https://leetcode-cn.com/problems/sort-colors/
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
必须在不使用库的sort函数的情况下解决这个问题。
示例 1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
分析:这个问题要用双指针,用p1指向第一个元素,用p2指向最后一个元素。然后对数组从左到右遍历。如果当前数是0,就和p1对应的数交换,然后p1右移。如果当前数是2,就和p2对应的数交换,然后p2左移,再判断交换过来的数是不是1,如果不是1的话,需要将i(当前遍历索引)回退一位。直到i和p2相遇为止。
class Solution {
public void sortColors(int[] nums) {
if(nums.length <= 1){return ;}
int p1 = 0;
int p2 = nums.length-1;
for(int i = 0; i <= p2; i++){
if(nums[i] == 0){
nums[i] = nums[p1];
nums[p1] = 0;
p1++;
}
if(nums[i] == 2){
nums[i] = nums[p2];
nums[p2] = 2;
p2--;
if(nums[i] != 1){
i--;
}
}
}
}
}
3.2 快速排序的核心思想
快速排序同样是递归算法,排序将分为三步:
第一步:随机选取数组中的一个数(不妨认为选第一个数),先将数组排序,使得比该数小的数在左侧,和该数相等的数在中间,比该数大的数在右侧。这其实就是我们3.1中讨论的荷兰国旗问题。
例:[4,6,2,4,5,3,1,5] --> [2,1,3],[4,4],[5,6,7]
第二步:对左半部分快速排序
第三步:对右半部分快速排序
3.3 快速排序的时间复杂度
最好情况:第一步能够将数组恰好等分,此时,即
最差情况:第一步以后,右半部分为空,此时,即
平均情况:我们将第一步完成后,分割点所处的位置视为一个随机变量,然后求数学期望(过程略),可知平均复杂度为