//原地归并
func merge(a []int, lo, mid, hi int) {
i, j := lo, mid+1
for k := lo; k <= hi; k++ {
aux[k] = a[k]
}
for k := lo; k <= hi; k++ {
if i > mid {
a[k] = aux[j]
j++
} else if j > hi {
a[k] = aux[i]
i++
} else if aux[j] < aux[i] {
a[k] = aux[j]
j++
} else {
a[k] = aux[i]
i++
}
}
}
先将所有元素都复制到aux中,然后再归并回a中。
在归并时(第二个for循环进行了4个条件判断:左半边用尽(取右半边的元素、右半边用尽(取左半边的元素)、右半边的当前元素小于左半边的当前元素(取右半边的元素)以及右半边的当前元素大于等于左半边的当前元素(取左半边的元素)。
var aux []int
func Sort(a []int) {
aux = make([]int, len(a))
sort(a, 0, len(a)-1)
}
func sort(a []int, lo, hi int) {
if hi <= lo {
return
}
mid := lo + (hi-lo)/2
sort(a, lo, mid) //左半边排序
sort(a, mid+1, hi) //右半边排序
merge(a, lo, mid, hi) //归并结果
}
下图为归并的具体过程:
接下来我想将go的协程特性运用在自顶向下的归并排序中测试一下性能是否会有提升。
package main
import (
"math/rand"
"time"
"./merge"
"github.com/BB-fat/gostopwatch"
)
func main() {
a := []int{}
seed := rand.NewSource(time.Now().Unix())
r := rand.New(seed)
for i := 0; i < 100000000; i++ {
a = append(a, r.Intn(100000000))
}
gs := gostopwatch.StopWatch{}
gs.NewTimer("计时器")
merge.Sort(a)
gs.NewPoint("计时器", "连接完成")
gs.PrintOneTimer("计时器")
}
利用我之前写的一个go秒表来给程序运行计时。
测试的数据量是一亿。
首先使用正常的自顶向下归并排序将这个随机生成的数组排序,结果耗时15.6秒。
func sort(a []int, lo, hi int) {
if hi <= lo {
return
}
mid := lo + (hi-lo)/2
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
sort(a, lo, mid) //左半边排序
wg.Done()
}()
go func() {
sort(a, mid+1, hi) //右半边排序
wg.Done()
}()
wg.Wait()
merge(a, lo, mid, hi) //归并结果
}
左半边右半边分别通过一个协程启动,并通过WaitGroup阻塞每一层的sort函数。
接下来运行测试脚本进行测试,结果耗时3分多!
并且在运行的时候cpu和内存使用均出现了异常。
go语言的协程特性使程序员可以很方便的使用它编写并发程序,这一点要清楚:是并发不是并行。并行是指同时有多个程序在运行,而并发是指多个函数在同一时间段运行,但是在同一时刻cpu只在执行一条语句。使用并发来解决问题所在的层次是在算法之上的,举个例子:一个程序有两个协程,其中一个要通过网络传输数据,另一个需要和硬盘交换数据,那么如果协程一在请求网络通信之后等待网络的返回,此时通过go的协程调度,程序可以先执行和硬盘交互的协程,等网络响应之后再继续执行协程一。
在这个问题中,因为归并排序中存在递归,所以程序在运行起来之后将创建几千万个协程,每一个协程都要分配相应的内存资源,而且程序在切换协程的时候也需要消耗一些性能,所以这么做不但没有优化算法,反而将电脑搞成平底锅,实在是愚蠢。
归并排序适用于长度较大的数组,对于长度较小的数组简单的排序方法会比归并排序更快,接下来我就设定在数组长度小于15的时候使用插入排序。
package merge
func Sort_insert(a []int) {
aux = make([]int, len(a))
sort(a, 0, len(a)-1)
}
func sort_insert(a []int, lo, hi int) {
if hi-lo <= 16 {
for i := lo; i < hi; i++ {
for j := i; j >= lo && a[j] < a[j-1]; j-- {
tmp := a[j]
a[j] = a[j-1]
a[j-1] = tmp
}
}
}
mid := lo + (hi-lo)/2
sort_insert(a, lo, mid) //左半边排序
sort_insert(a, mid+1, hi) //右半边排序
merge(a, lo, mid, hi) //归并结果
}
编写一个测试脚本来验证这个猜想,测试数据大小是10亿。
func main() {
a := []int{}
seed := rand.NewSource(time.Now().Unix())
r := rand.New(seed)
for i := 0; i < 1000000000; i++ {
a = append(a, r.Intn(1000000000))
}
b := make([]int, len(a))
copy(b, a)
// fmt.Println(a)
gs := gostopwatch.StopWatch{}
gs.NewTimer("普通归并排序")
merge.Sort(a)
// fmt.Println(a)
gs.NewPoint("普通归并排序", "完成")
gs.NewTimer("优化归并排序")
merge.Sort_insert(b)
gs.NewPoint("优化归并排序", "完成")
gs.PrintAllTimers()
}
普通归并排序
完成 4m31.531979145s
优化归并排序
完成 4m14.386619368s
速度提升了10%左右
实现归并排序的另一种方法是先归并那些微型数组,然后再成对归并得到的子数组,如此这般,直到我们将整个数组归并在一起。这种实现方法比标准递归方法所需要的代码量更少。首先我们进行的是两两归并(把每个元素想象成一个大小为1的数组),然后是四四归并(将两个大小为2的数组归并成一个有4个元素的数组),然后是八八的归并,一直下去。
merge函数同自顶向下归并排序。
func SortBU(a []int) {
aux = make([]int, len(a))
n := len(a)
for sz := 1; sz < n; sz += sz {
for lo := 0; lo < n-sz; lo += sz + sz {
merge(a, lo, lo+sz-1, int(math.Min(float64(lo+sz+sz-1), float64(n-1))))
}
}
}
自底向上归并排序图解:
本节代码