Go源代码学习:sort 归并排序实现
在sort包中实现的归并排序方法叫Stable 排序:
相关函方法:
insertionSort
stable
symMerge
swapRange
rotate
详细介绍:
1、insertionSort(data Interface, a, b int)
简单的插入排序,这里不做细节解释:原理是假设前半段数据是有序的,把i插入到已经有序的前半段中,这里是使用两层循环实现。
2、swapRange(data Interface, a, b n, int)
交换data[a:a+n] data[b:b+n],功能简单暴力,没有什么难度
3、rotate(data Interface, a, m , b int)
方法功能如例:
2,3,4,5,1,6,8 -> 5,1,6, 2,3,4,8
a m b m a b
实现方法:
第一步:计算 m-a和 b-m 的长度 i,j
第二步:i!=j时,先调换较小的一段, i>j时,调用swapRange(data, m-i, m, j), 此时我们将,a:m:b两段 分成三段 m-i:m-i+j:m:m+j,我们把m-i:m-i+j与m:m+j使用rotate函数,就是
swapRange(data, m-i, m, j);i<j时,a:m:b两段 分成三段m-i:m:m+j-i:m+j,把m-i:m 与m+j-i与m+j使用rotate函数,就是swapRange(data,m-i, m+j-i; i);其实以上过程用一句话概括就是,在两段长度不同的情况下,可以一次将较短的一段转移到目标位置,较长的一段则发生了前后交换,所以进入下一个循环去解决较长一段的交换,使其恢复。
第三步:第二部结束后一定会发生i=j的情况,极端情况下就是i==j==1,此时使用swapRange函数将前后两段交换就解决问题了。
4、stable(data Interface, n int)
第一步:按照每20个数据为一组进行组内的插入排序,最后不足20个数据需要特殊处理一下。
第二步:外层循环每一次将一组的范围扩大一倍,内层循环将相邻两组归并排序,最后不足两组的需要特殊处理。
备注:其实这个函数的实现逻辑很简单,但是谷歌大神们在标准库里实现的排序为什么会用20最为一个基数,就很不理解了。
5、symMerge (data Interface, a, m, b int)
这个算法实现是来源于这篇论文:Stable Minimum Storage Merging by Symmetric Comparisons。
go在最早的c实现版本就已经使用了这个归并算法,其实这个算法的思想很简单,举个例子来说:假设有两个有序数等长组a,b
a: 11, 12, 13, 14, 15, 21, 22, 23
b: 11, 12, 13, 16, 17, 18, 19, 20
可以看出来a的后面3个元素每一个都大于b的前三个元素,那我们就可以把a,b的对应的这6个元素交换,交换之后的两个数组分别为
a: 11, 12, 13, 14, 15, 11, 12, 13
b: 21, 22, 23, 16, 17, 18, 19 ,20
此时我们可以看出来a中的每一个元素都小于b中的元素。
这就是这个算法的主要思想:将较大的元素都放到b中,较小的都放在a中。
那么我们要怎么找到这些需要交换元素呢?
因为我们的数据是有序的,所以我们只要从a最后一个元素和b的第一个元素开始,依次取一个元素比较大小就可以了,写个伪代码:
for i:=len(a); i>0; i--{
if b[len(b)-i] > a[i] {
break;
}
}
找到第一个i对应的元素是b中的大时即可退出循环。
当然这只是方便理解,论文中的算法实现是用的二分查找,但上来就用二分查找可能不是很好理解,所以我在这里先用简单的遍历来解释。
以下是源代码加上个人的理解注释:
package sort
func Stable(data Interface) {
stable(data, data.Len())
}
func stable(data Interface, n int) {
// 定义最小merge的块大小为20
// 此处为什么是20,不知道是不是作者的幸运数字
blockSize := 20 // must be > 0
a, b := 0, blockSize
for b <= n {
// 调用插入排序将每一块数据内部排序
insertionSort(data, a, b)
a = b
b += blockSize
}
//最后不足20个的块特殊处理
insertionSort(data, a, n)
for blockSize < n {
// 以blocksize作为基数,相邻两块为一组进行归并
a, b = 0, 2*blockSize
for b <= n {
symMerge(data, a, a+blockSize, b)
a = b
b += 2 * blockSize
}
//剩余的不足一组的,如果个数大于基数blocksize,特殊处理
if m := a + blockSize; m < n {
symMerge(data, a, m, n)
}
//扩大基数,进行下一次循环
blockSize *= 2
}
}
// 插入排序
func insertionSort(data Interface, a, b int) {
// a < i && i < b
for i := a + 1; i < b; i++ {
// a < j 假设data[a:m]已经有序,
// 将data[j]插入到data[a:m]中
for j := i; j > a && data.Less(j, j-1); j-- {
data.Swap(j, j-1)
}
}
}
// 前后等量对调
func swapRange(data Interface, a, b, n int) {
// data[a:a+n] <-> data[b:b+n]
for i := 0; i < n; i++ {
data.Swap(a+i, b+i)
}
}
// 实现了Stable Minimum Storage Merging
// by Symmetric Comparisons 算法
// 此方法比较难解释清楚,只能尽量去解释
// 如果没看懂,可以去拜读一下这篇论文
func symMerge(data Interface, a, m, b int) {
// 当要归并的前面一组只有一个元素时,直接折半插入
if m-a == 1 {
i := m
j := b
for i < j {
// 计算mid下标数
h := int(uint(i+j) >> 1)
// 折半查找
if data.Less(h, a) {
i = h + 1
} else {
j = h
}
}
//data[a]插入到目标位置i - 1
for k := a; k < i-1; k++ {
data.Swap(k, k+1)
}
return
}
// 当要归并的后面一组只有一个元素时,直接折半插入
if b-m == 1 {
i := a
j := m
for i < j {
// 计算mid下标数
h := int(uint(i+j) >> 1)
// 折半查找
if !data.Less(m, h) {
i = h + 1
} else {
j = h
}
}
// data[m] 插入到目标位置 i + 1
for k := m; k > i; k-- {
data.Swap(k, k-1)
}
return
}
// 下方高能
// 首先,在两个有序段中,以较短的段长为准,在mid周围找到另一个与之等长的有序段。
// 这里不好理解,可以将所有变量改为用mid,a,b,m来表示。
mid := int(uint(a+b) >> 1)
n := mid + m
var start, r int
if m > mid {
start = n - b
r = mid
} else {
// 我只在这种情况下做解释, 此时m<mid
// 说明a,b的mid在m的右侧,也说明a:m是比较短的一段
start = a
r = m
}
// p = mid+m-1
p := n - 1
for start < r {
// c 是折半查找时的下标,所以在m<mid时c的取值范围就是 start<c<r即,a<c<m
c := int(uint(start+r) >> 1)
// p-c=mid+m-1-c
// 不明白p-c是指什么,可以将c的取值范围带入
// 得到 p-c 的取值范围是 mid+m-1-m < p-c < mid+m-1-a
// 可以看出来p-c其实是对应 mid-1:mid-1+(m-a)的一段
// 而c的取值范围可以写成 a<c<a+(m-a)
// 所以p-c与c其实分别在两段长度都等于m-a的有序段中
if !data.Less(p-c, c) {
start = c + 1
} else {
r = c
}
} //上面这个循环的解释写在这里:
// 这个循环看起来像是个折半查找,但是target不是一个确定的量,而是p-c这个下标的元素
// 而c是一个变量,p-c与c分别属于位于两个等长的段a:a+m-a,mid-1:mid-1+m-a
// c取最大值时p-c取最小值,可以看出来,这段其实是正文中提到的Stable Minimum Storage Merging
// by Symmetric Comparisons 算法的一个实现
// 即在a:m中从后往前,找到连续的n个元素每一个都大于mid-1:mid-1+m-a中从前往后的n个元素
// 上面的循环是在两个等长的有序段找到可以替换的部分
// 而实际情况是m:mid-1这一段元素也小于a:m的后n个元素
// 所以可以交换这两段不等长的数据
end := n - start
if start < m && m < end {
rotate(data, start, m, end)
}
// 交换后的两段本身都是有序的,交换后与没有变动的元素组成了两组
// 即a:start,start:mid 和mid:end,end:b
// 分别对两段再进行归并
if a < start && start < mid {
symMerge(data, a, start, mid)
}
if mid < end && end < b {
symMerge(data, mid, end, b)
}
// 以上解释都没有对边界条件进行考虑
}
// 不等量前后对调
func rotate(data Interface, a, m, b int) {
// 计算 两段需要对调的段长
i := m - a
j := b - m
//两段不等长时
//先将较短的一段移动到目标位置
for i != j {
if i > j {
//此时前一段比较长
//将从m开始的j个元素与从m-i开始的j个元素对调
swapRange(data, m-i, m, j)
//此时原来后面j个元素已经到达最前面
//而原来前面的i个元素的前j个元素跑到了最后面
//调整i
i -= j
//在下一个循环对调前面i,j个元素
} else {
//此时后一段比较长
//将从m-i开始的i个元素与从m+j-i开始的i个元素对调
swapRange(data, m-i, m+j-i, i)
//此时原来前面的i个原色已经到达最后面
//而原来后面j个元素的后i个元素跑到了最前面
//调整j
j -= i
//在下一个循环对调前面i,j个元素
}
}
// i==j 循环结束时,直接将m前后的i个元素对调
swapRange(data, m-i, m, i)
}