Go源代码学习之sort包 (一)

本文详细解析了Go语言sort包中的归并排序实现,包括insertionSort、swapRange、rotate和stable方法,重点介绍了stable方法的分组插入排序和对称归并策略,以及symMerge算法的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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)
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值