更新摊还分析方法分析时间复杂度
归并模板
归并属于分治算法,有三个步骤
void merge_sort(int q[], int l, int r)
{
//递归的终止情况
if(l >= r) return;
//第一步:分成子问题
int mid = l + r >> 1;
//第二步:递归处理子问题
merge_sort(q, l, mid ), merge_sort(q, mid + 1, r);
//第三步:合并子问题
int k = 0, i = l, j = mid + 1, tmp[r - l + 1];
while(i <= mid && j <= r)
if(q[i] <= q[j]) tmp[k++] = q[i++];
else tmp[k++] = q[j++];
while(i <= mid) tmp[k++] = q[i++];
while(j <= r) tmp[k++] = q[j++];
for(k = 0, i = l; i <= r; k++, i++) q[i] = tmp[k];
}
待证问题: tmp
保存的是 q[l..mid]
, q[mid+1..r]
中从小到大排序的所有数
证明(第一个 while
循环)
循环不变式: tmp[0..k-1]
保存上述俩数组中从小到大排序的最小 k 个数
-
初始
k = 0, tmp[0..k-1]
为空,显然成立 -
保持
假设某轮循环开始之前,循环不变式成立
若
q[i] <= q[j]
, 则tmp[k] = q[i]
其中,
q[i] <= q[i+1..mid], q[i] <= q[j] <= q[j+1..r]
∴
q[i]
是剩下的所有数中最小的一个当
q[i] > q[j]
时,同理可以得到tmp[k] = q[j]
是剩下数中最小的一个∴
tmp[k]
是剩下数中最小的一个∴
k
自增之后,下轮循环开始之前,tmp[0..k-1]
保存从小到大排序的最小k
个数 -
终止
i > mid 或 j > r
则
q[l..mid]
和q[mid+1..r]
其中一个数组的数都已遍历tmp[0..k-1]
保存从小到大排序的最小k个数
边界分析
-
为什么不用
mid - 1
作为分隔线呢即
merge_sort(q, l, mid - 1 ), merge_sort(q, mid, r)
因为
mid = l + r >> 1
是向下取整,mid
有可能取到l
(数组只有两个数时),造成无限划分解决办法:
mid
向上取整就可以了, 即mid = l + r + 1 >> 1
,如下所示:
void merge_sort(int q[], int l, int r)
{
if(l >= r) return;
int mid = l + r + 1>> 1;//注意mid是向上取整
merge_sort(q, l, mid - 1 ), merge_sort(q, mid, r);
int k = 0, i = l, j = mid, tmp[r - l + 1];
while(i < mid && j <= r)
if(q[i] <= q[j]) tmp[k++] = q[i++];
else tmp[k++] = q[j++];
while(i < mid) tmp[k++] = q[i++];
while(j <= r) tmp[k++] = q[j++];
for(k = 0, i = l; i <= r; k++, i++) q[i] = tmp[k];
}
不过最好不要这样写,很奇葩,不对称
-
为什么 用
mid
作为分隔线时不会造成无限划分呢因为此时
mid
是向下取整的,merge_sort(q, l, mid )
中的mid
一定不会取到 r 值∴
merge_sort(q, l, mid )
不会无限划分
摊还分析
摊还分析是一种分析时间复杂度的方法
主要有三种:
- 聚合分析(记账法)
- 核方法
- 势能法
聚合分析(记账法)最符合直观感觉,
聚合分析归并排序的时间复杂度
归并排序属于分治法, 很容易写出递归式:
$T(n) = 2T(n/2) + f(n)$
其中, $2T(n/2)$ 是子问题的时间复杂度, $f(n)$ 是合并子问题的时间复杂度
1.直观
直观上我们感觉 $f(n) = O(n)$, 事实也正是如何, 因为每次 while
都会把一个元素添加到数组中, 一共有 n 个元素, 所以 while
循环的次数为 n , 时间复杂度为 $O(n)$
2.摊还分析的聚合分析
对于每次迭代中选出并添加到数组中的元素, 我们给它的摊还代价设为 1(记账为 1)
一个元素只能计费一次, 因为马上就被添加到数组中了
一共有 n 个元素, 所以摊还总代价为 n, 算法的时间复杂度为 $O(n)$
摊还代价, 我们自己设定的一个理想代价, 只有一个要求: 总的摊还代价大于总的实际代价, 所以总摊还代价是总实际代价的上界
实际代价, 实际操作的代价
3.计算归并排序的递归式
得到 $f(n) = O(n)$ 后, 根据递推式的计算方法(代入法, 递归树法, 主方法)容易计算出 $T(n) = O(n\log n)$, 即归并排序的时间复杂度为 $O(n\log n)$
总结归并思路
-
有数组 q, 左端点 l, 右端点 r
-
确定划分边界 mid
-
递归处理子问题 q[l..mid], q[mid+1..r]
-
合并子问题
-
主体合并
至少有一个小数组添加到 tmp 数组中
-
收尾
可能存在的剩下的一个小数组的尾部直接添加到 tmp 数组中
-
复制回来
tmp 数组覆盖原数组
-
关于for(k = 0, i = l; i <= r; k, i) q[i] = tmp[k];这一行反而不是很懂
l,r的元素排序好之后是在一个临时数组里,这就是把临时数组中的元素复制到目标数组里啊
i为什么不可以等于0啊,不懂
同问
因为前面说明了,我们排序的数组的区间是[l , r ],那么吧这段区间内的数字排序好以后还是只能放在这段区间里面的
我打印了l,r,,因为是递归,这个的思想是先拆成最小单位,然后再一步步往回归并,如果i = 0,每次归并都会初始化i=0,范围0-4,所以会出问题,
那最终输出为啥不能直接遍历输出tmp数组中的值啊
因为不是目标数组
先不管是不是目标数组,下面直接输出tmp数组,结果也是不对的
因为tmp是每次递归中使用的临时数组,每次调用归并函数的时候位置都会改变
懂了Orz
是怎样让两边的数从小到大排序的,不是很懂,我只理解到了第三步
当区间被划分到只有一个元素时就自动有序了,然后再将这些有序的区间合并起来
所以叫归并排序
为什么快排分界是 q[l + r >> 1],而归并排序是(l + r >> 1)
快排 不一定就是取q[l + r >> 1],它可以取q[l]q[r],取q[l+r>>]只是为了防止边界问题,并且当用i分界是 l +r >> 要想上取整,也就是l + r + 1 >> 1 不然当 l + r >> 1 == l 的时候,同时循环过后i = l(也就是l + 1 l + 2 … j 上面的数字全部大于q[l]) 其中一个分治问题还是quick_sort(l,r) 因为分成了 l,i-1 i,r i == l,所以第二个区间会一直循环。
而归并排序是分成两个子问题解决,我们从下往上 把子问题全部解决,最后这个大问题也就解决了。
各位佬,你们一般写递归,怎么判定它的正确性,我一般写出来都是很模糊的感觉,哎
确定递归出口和一棵递归树
如果我定义了一个全局变量tmp[N],那函数中定义 “ tmp[r - l + 1] ”这个还有必要吗?
那也是局部变量不冲突,但是没必要
为什么上面用while循环都判断一遍(while(i < mid && j <= r))了,下面还要再判断while(i < mid)和while(j <= r)?
while(i < mid && j <= r)确保在i<=mid并且j<=r的情况下,才将q[i]和q[j]进行比较,当其中一个条件不满足时跳出这个循环,此时一定有i<=mid或者j<=r(二者取其一),再将剩下的元素加到tmp数组里
谢谢!!!
https://www.acwing.com/solution/content/138606/
梦开始的地方!
for(k = 0, i = l; i <= r; k, i) q[i] = tmp[k];”i<r”行吗?
非常棒的讲解谢谢
请问这句话里的 int k = 0, i = l, j = mid, tmp[r - l + 1];
定义的变量i=l,l是什么意思呢,l是得什么值呢
将 i 赋值为 这一次递归中的左端点
可以换一种同步原数组更好理解的写法
https://www.acwing.com/solution/content/252826/
orz
它为什么一定要拷贝数组呢
原数组是无序的,新数组是原数组的部分有序片段,把它拷贝回去就最后组成了一整个有序数组
为什么if(a[i]<=a[j]) 而不是if(a[i]<a[j])
为了保持稳定性,相同的值先排前面的数
懂了谢谢哈
请问我这个拷贝问题出在哪,3 1 2 4 5给的样例结果是 00123
for(i = left ; i<= right; i++){
a[i]=temp[i];
}
temp
序列下标应该是从0
开始的,你的代码中temp
序列0-i-1
的部分没有使用,因此是错误的嗷嗷,明白了,谢谢你喔
写的好!
为什么最后将数组拷贝的时候的终止条件是i<=r而不能是j<=k呢
拷贝这块我也迷糊
这里的k是个数,应该用j<k,这就没问题
谢谢,解惑了
递归是怎么处理子问题的?
你把第一和第二步看成一个整体,第三步是回溯的过程不用管…第一第二步整体理解就是不断对数组的中间切割,最终切割成很多个只有两个数数组
第二步 递归处理子问题是处理子函数中l>=r的问题嘛
nb