持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第二天,点击查看活动详情
最近在看左神的数据结构与算法,考虑到视频讲的内容和给出的资料的PDF有些出入,不方便去复习,打算出一个左神的数据结构与算法的笔记系列供大家复习,同时也可以加深自己对于这些知识的掌握,该系列以视频的集数为分隔,今天是第二篇:P4|认识O(NlogN)的排序
一、Master公式
在算法中,递归是“神”的工具,尽管它代码更为简介,但是由于不断的调用自己,也导致函数的时间复杂度不方便计算,因此就有了
Master
公式
1、📜 公式
T(N) = aT(N/b) + O(N^d)
① 公式组成
T(N)
指母问题的数据量是N
级别的,即母问题一共有N
个数据a
为子问题的调用次数b
为样本的分层N/b
为子问题的规模d
为除去调用之外,剩下过程的时间复杂度
② 计算方法
log(b,a) > d
==> 时间复杂度为:O( N^log(b,a) )
log(b,a) < d
==> 时间复杂度为:O( N^d )
log(b,a) = d
==> 时间复杂度为:O( N^d * logN )
2、🎯 举例:找到数组中的最大值
public static int process(int[] arr, int L, int R){
if(L == R){
return arr[L];
}
int mid = L + ((R-L) >> 1);
int leftMaxNum = process(arr, L, mid);
int rightMaxNum = process(arr, mid, R);
return Math.max(leftMax, rightMax);
}
- 首先可以认为
process
为母问题 - 对于一次递归,它对数据进行二分,即原有的样本量被分成了两份,即
T(N) = aT(N/2) + O(N^d)
,因此我们可以得到b = 2
- 两份样本的样本量都是
N/2
,即该样本执行了两次,即T(N) = 2T(N/2) + O(N^d)
,因此我们可以得到a = 2
- 处理完子问题之后,只有
Math.max
一个过程,时间复杂度为 O(1) - 其中
log(b,a) = log(2,2) = 1
d = 0 < log(b,a)
- 故时间复杂度为
O(N^1) = O(N)
二、归并排序
1、💫 实现思路
- 分别对左半部分和右半部分做排序,使左边和右边分别有序
- 在对一半样本量做排序的时候,就使用递归
- 递归退出的条件为
left == right
- 左边和右边有序后,引入一个
help
数组,使用双指针对左半部分和右半部分做遍历,小的数据先进help
数组,最后把help
数组的数据拷贝到原数组
2、💻代码实现
public static void mergeSort(int[] array) {
if (array.length < 2){
return;
}
mergeSort(array,0,array.length - 1);
}
public static void mergeSort(int[] array, int left, int right){
if (left == right){
return;
}
int middle = left + (right - left) / 2;
// 对左右两边的部分,分别递归做好排序
mergeSort(array,left,middle);
mergeSort(array,middle + 1, right);
merge(array,left,right,middle);
}
// 使用help数组,以及双指针做遍历
public static void merge(int[] array, int left, int right, int middle){
// 左半部分的头指针p1 右半部分的头指针p2
int p1 = left, p2 = middle + 1, i = 0;
int[] help = new int[right - left + 1];
// 遍历原数组,把小的数先放进去
while (p1 <= middle && p2 <= right){
help[i++] = array[p1] < array[p2] ? array[p1++] : array[p2++];
}
// 如果有一边所有都放进去了,就把另外一半直接放进去
while (p1 <= middle){
help[i++] = array[p1++];
}
while (p2 <= right){
help[i++] = array[p2++];
}
// 把 help 数组拷贝到原数组
System.arraycopy(help, 0, array, left, help.length);
}
3、🚩 时间复杂度分析
不难发现,归并排序使用到了递归的算法,要计算时间复杂度,就需要用到前文中提到的
Master
公式
- 首先每次把数据分为两份,而且执行两次 即
a = 2,b = 2
- 而除了子问题外,还有一个操作
merge
,该过程相当于对所有的数据做了遍历,即时间复杂度为O(N)
即d = 1
- 那么
log(a,b) = log(2,2) = 1 = d
- 即时间复杂度为
O(N * logN)
三、小和问题
小和问题其实是归并排序衍生出来的题目,与归并排序的思想相似 题目:在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和 例子:[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、💫 解题思路
- 与其看左边有多少个数比自己小,不妨转变思路,看右边有多少个数比自己大
- 我们在做
merge
的时候,会比较左右两边的数,只需要在做比较的时候,把右边比自己大的数记录下来并计算即可 - 比如左边的数组为 1 2 3 右边为 2 5 6
- 在左边指针为 1 的时候,发现比右边的数 2 小,就用最右边数的索引值减去当前右边指针所在位置,即 2 的位置的索引,得到 3,即有 3 个数比 1 大
- 然后继续后移 左边指针为 2 右边数的指针为 2,然后把右边的 2 放进辅助数组 help,然后右指针继续移动,发现有 2 个数比 2 大
- 依次类推…………
2、💻 代码实现
public static int smallSum(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
return mergeSort(arr, 0, arr.length - 1);
}
public static int mergeSort(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + ((r - l) >> 1);
return mergeSort(arr, l, mid)
+ mergeSort(arr, mid + 1, r)
+ merge(arr, l, mid, r);
}
public static int merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
int res = 0;
while (p1 <= m && p2 <= r) {
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
System.arraycopy(help, 0, arr, l, help.length);
return res;
}
- 不难发现,其实大体上的代码与归并排序一致,但是修改了一些逻辑
- 首先可以发现
merge
有了返回值,就是本次合并中得到的小和 - 然后
mergeSort
中也有了返回值,返回的是左边的小和、右边的小和、左边与右边结合起来得到的整体的小和 - 此处是不会有重复的,大家可以去debug调试一下
- 然后求小和的方法就如上面实现思路所说:
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
- 采用下标相减,然后乘左边的数据的操作
- 首先可以发现
3、🌈 逆序对
逆序对:在一个数组中,左边的数如果比右边的数大,那么两个数构成一个逆序对 其实不难发现,这个与小和问题有异曲同工之妙,同样是在merge的时候去比较两边的数,在做交换的时候,打印逆序对即可
四、荷兰国旗问题
荷兰国旗问题是为了后面的快速排序打基础
1、荷兰国旗问题一
①📑 题目
给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的 数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
②💫 实现思路
- 在左边维护一个
Less
空间,遍历数组,与num
做比较,如果小于等于num
就把数据放进less
,并且less++ pivot++
- 如果大于
num
,就不扩张less
空间,只是指针右移,继续遍历
③💻 代码展示
public static void netherlandsFlag(int[] array, int number){
int left = 0;
int less = -1;
for (int i : array) {
if (i <= number){
swap(array, left++, ++less);
}else {
left++;
}
}
}
2、荷兰国旗问题二
①📑 题目
给定一个数组arr
,和一个数num
,请把小于num
的数放在数组的左边,等于num
的数放 在数组的中,大于num
的数放在数组的右边。要求额外空间复杂度O(1)
,时间复杂度 O(N)
②💫 实现思路
- 有了上面的问题一,其实我们难想到这题的解决方法:
- 在右边也维护一个空间,用于存放大于
num
的数据 - 即 如果小于就存在左边的空间 大于就存在右边的空间 等于就不存 只是
pivot++
继续右移
③💻 代码展示
public static void netherlandsFlag(int[] array, int number){
int left = 0;
int less = -1;
int right = array.length - 1;
int more = right + 1;
while (left < more){
if (array[left] > number){
swap(array,left,--more);
}else if (array[left] < number){
swap(array,left++,++less);
}else {
left++;
}
}
}
五、快速排序
1、💫 实现思路
1️⃣ 快排 1.0
- 在整个数组中,拿最后一个数做划分值,即作为 num,让最后一个数前面的一段做到,小于等于num的放左边,大于num的放右边
- 然后把这个数num与大于num的第一个数做交换,此时就能做到左侧的都是小于等于num的数,右侧都是大于num的数
- 然后让左侧和右侧都重复这个行为
2️⃣ 快排 2.0
- 快排 2.0实际上就是荷兰国旗问题二,与快排 1.0 的区别在于,分段的时候是:小于num的放左边,等于num的放中间,大于num的放右边
- 不管是快排1.0还是快排2.0,时间复杂度都是O(N²),因为可以举出最差的例子:1、2、3、4、5、6、7、8、9,当完全逆序的时候,分别拿9、8、7、6、5、4、3、2、1做划分值,每次partition都只搞定了一个数
3️⃣ 快排 3.0
- 上述的差的情况是因为划分值打的很偏,好的情况其实就是打在中间的位置,比如上述的例子,打到5,那么其实左右两侧的规模是差不多的,此时的表达式为:
T(N) = 2T(N/2) + O(N)
- 时间复杂度为
O(N * logN)
- 因此快排3.0从数组中随机选出一个值,放在最后的位置,那么此时好情况和差情况就变成了概率事件
- 现在把所有的情况使用概率论并且求数学长期期望,就能得到时间复杂度为:O(N * logN)
2、💻代码实现
public static void quickSort(int[] array) {
if (array.length < 2){
return;
}
quickSort(array,0,array.length - 1);
}
public static void quickSort(int[] array, int left, int right){
if (left >= right){
return;
}
swap(array, left + (int) (Math.random() * (right - left + 1)), right);
int[] partition = partition(array, left, right);
quickSort(array, left, partition[0]);
quickSort(array,partition[1], right);
}
public static int[] partition(int[] array, int left, int right){
int less = left - 1;
int more = right;
while (left < more){
if (array[left] < array[right]){
swap(array, ++less, left++);
} else if (array[left] > array[right]){
swap(array, left, --more);
} else if (array[left] == array[right]){
left ++;
}
}
swap(array,more,right);
return new int[]{less,more+1};
}