创建时间: 2019/10/8 20:48
作者: sunpengwei1992@aliyun.com

思考一下,归并排序适合哪些场景?哪里有用到归并排序

归并排序是一种分治思想的应用,所以也适合处理大数量的排序,因此也是一种外排序算法,磁盘排序算法,应用场景也较多,比如mysql的排序,sharding-jdbc的排序,

  • 下面文字是shardding-jdbc官网的一段描述,官网地址如下,友情提示:官网左边导航栏最下方支持中英文语言切换哦
    1. https://shardingsphere.apache.org/document/current/cn/features/sharding/principle/merge/
    归并排序,由于在SQL中存在ORDER BY语句,因此每个数据结果集自身是有序的,因此只需要将数据结果集当前游标指向的数据值进行排序即可。 这相当于对多个有序的数组进行排序,归并排序是最适合此场景的排序算法。
    该算法是将已有的子序列不断进行合并从而最终达到全局有序,一般我们的实现都是二路归并,就是两个有序的子序列进行合并,但也可以进行多路归并(将大于两个的子序列进行合并)
    我们通过一个简单的归并排序(递归)来分析时间,空间复杂度
    1. func MergeSort(array []int)[]int{
    2. if len(array) <= 1{
    3. return
    4. }
    5. middle := len(array) / 2
    6. //不断缩小待排序的数组的范围大小
    7. left := MergeSort(array[:middle])
    8. right := MergeSort(array[middle:])
    9. //开始二路归并
    10. return merge(left,right)
    11. }
    12. func merge(left,right []int) (result []int){
    13. l,r := 0,0
    14. for l < len(left) && r < len(right){
    15. if left[l] < right[r]{
    16. result = append(result,left[l])
    17. l++
    18. continue
    19. }
    20. if left[l] > right[r]{
    21. result = append(result,right[r])
    22. r++
    23. continue
    24. }
    25. result = append(result,left[l])
    26. result = append(result,right[r])
    27. l++
    28. r++
    29. }
    30. for l < len(left){
    31. result = append(result,left[l])
    32. l++
    33. }
    34. for r < len(right){
    35. result = append(result,right[r])
    36. r++
    37. }
    38. return result
    39. }
    我们分析上面的代码,看看归并排序的复杂度怎么样呢,首先递归的终止条件必须确定,就是数组大小小于等于1时,递归终止,我们不断通过对待排序的数据array进行折中,从而达到最终二路归并时left,right的数组大小都是1,我们看下图,方便大家理解递归和归并,由图得知,我们每次对数组拆分都是一分为二的才做,比如数组长度为4,拆分到最后为1时就是4/2/2的操作,所以递归拆分的时间复杂度是logN(以2为底),在归并时是对两个有序的序列开始做合并,递归了n次,所以要合并n次,但每次合并时遍历子序列,假设子序列长度为n,所以整体时间复杂度为nlogN,每次合并时申请新的空间存储合并后的有序数组返回,所以空间复杂度为O(n),
    排序-归并排序,一种外排序,递归,非递归,磁盘? - 图1
    利用数求递归的复杂度,这是一种简单思想,现在,我们需要知道这棵树的高度 h,用高度 h 乘以每一层的时间消耗 n,就可以得到总的时间复杂度n * h,而满二叉树的高度是log2(n),所以时间复杂度一目了然。
    排序-归并排序,一种外排序,递归,非递归,磁盘? - 图2

    复杂度总结

    时间复杂度:nlog2(n)
    空间复杂度:O(n)

    除了递归实现,你能想到非递归怎么实现吗?

非递归实现二路归并排序

一般非递归代替递归,递归其实利用了操作系统的栈空间存储临时数据,所以两种方案,一是利用栈数据结构,二是利用变量(这种相对局限一点)

  1. func MergerSortNotRecursion(array []int) []int {
  2. length := len(array)
  3. if length <= 1 {
  4. return array
  5. }
  6. i := 1 // 子序列大小
  7. result := make([]int, 0)
  8. for i < length {
  9. j := 0
  10. //按顺序两两归并,j用来定位起始点
  11. //随着系序列大小翻倍,每次两两归并的数组大小也在翻倍
  12. for j < length {
  13. if j+2*i > length {
  14. result = merge(array[j:j+i], array[j+i:length])
  15. index := j
  16. for _, v := range result {
  17. array[index] = v
  18. index++
  19. }
  20. } else {
  21. result = merge(array[j:j+i], array[j+i:j+2*i])
  22. index := j
  23. for _, v := range result {
  24. array[index] = v
  25. index++
  26. }
  27. }
  28. j = j + 2*i
  29. }
  30. i = i << 1 // 子序列大小翻倍
  31. }
  32. return array
  33. }

分析一下上面的代码的时间复杂度还是nlog2(n)吗? 答案是是的,自己分析一下哦

磁盘文件归并排序(也就是经常说的1亿数据,10M内存,请排序)

核心思路(多路排序)

  1. 每次读取一定量的数据(10M内存能放下),排序后单独写入小文件,直到大文件全部排完序写入很多(不再是两个,所以多路排序)个小文件,这时所有小文件中的数据是有序的
  2. 所有小文件中的数据每次读取一个做归并排序写入最终的结果文件中,直到所有小文件都处理完成

    不多说,贴代码,看代码说事

  1. //读取大文件排序写入临时文件
  2. func RedFileSortWriteTempFile(fileName string) {
  3. file, _ := os.Open(fileName)
  4. bufio := bufio.NewReader(file)
  5. //按行读取
  6. bs, _, _ := bufio.ReadLine()
  7. array := make([]int, 0, 0)
  8. suffix := 1
  9. for len(bs) > 0 {
  10. //NUMBER_TO_SORT,最多读取这么多个数据
  11. if len(array) == NUMBER_TO_SORT {
  12. //排序
  13. array = QuickSort2(array)
  14. // 写入文件
  15. WriteTempFile(array, suffix)
  16. //重新创建数组
  17. array = make([]int, 0)
  18. suffix++
  19. }
  20. number, _ := strconv.Atoi(string(bs))
  21. array = append(array, number)
  22. bs, _, _ = bufio.ReadLine()
  23. }
  24. //对最后不足最多个数的数组排序,写入文件
  25. array = QuickSort2(array)
  26. WriteTempFile(array, suffix)
  27. //对各个小文件开始归并排序
  28. MergerSortFile("E://temp/")
  29. defer file.Close()
  30. }
  1. //多个已经排好序的小文件合并排序,入参是小文件的父级目录
  2. func MergerSortFile(childFilePath string) {
  3. //最终生成的排序文件
  4. sortFile, _ := os.Create("E://finally_number_sort.txt")
  5. sortFileBufio := bufio.NewWriter(sortFile)
  6. //读取排好序的小文件数组
  7. fileInfos, _ := ioutil.ReadDir(childFilePath)
  8. if len(fileInfos) == 0 {
  9. return
  10. }
  11. //用来存放每个小文件的最小值
  12. minArray := make([]int, len(fileInfos))
  13. fileArray := make([]*os.File, len(fileInfos))
  14. //每个小文件的bufio指针
  15. fileBufio := make([]*bufio.Reader, len(fileInfos))
  16. //用来标识小文件是否读到末尾
  17. isEnd := make([]bool, len(fileInfos))
  18. for index, fileInfo := range fileInfos {
  19. file, _ := os.Open("E://temp/" + fileInfo.Name())
  20. fileArray[index] = file
  21. fileBufio[index] = bufio.NewReader(file)
  22. }
  23. startSort(fileArray,minArray,isEnd,sortFileBufio,sortFile)
  24. }
  1. //开始排序
  2. func startSort(fileArray []*os.File,minArray []int,isEnd []bool,
  3. sortFileBufio *bufio.Writer,sortFile *os.File,fileBufio []*bufio.Reader){
  4. for {
  5. flag := true
  6. for i := 0; i < len(fileArray); i++ {
  7. if !isEnd[i] && minArray[i] == 0 {
  8. bs, _, _ := fileBufio[i].ReadLine()
  9. if len(bs) == 0 {isEnd[i] = true}
  10. value, _ := strconv.Atoi(string(bs))
  11. minArray[i] = value
  12. }
  13. }
  14. //找出多个小文件的最小值的最小值
  15. minValue := GetMin(minArray)
  16. //写入最终文件中
  17. if minValue != 0 {
  18. sortFileBufio.WriteString(strconv.Itoa(minValue) + "\r\n")
  19. sortFileBufio.Flush()
  20. }
  21. //判断所有文件是否读取完毕
  22. for _, temp := range isEnd {
  23. if !temp {
  24. flag = false; break;
  25. }
  26. }
  27. //如果完毕,终止循环
  28. if flag {break}
  29. }
  30. //关闭流
  31. defer func() {
  32. sortFile.Close()
  33. for _, f := range fileArray {
  34. f.Close()
  35. }
  36. }()
  37. }
  1. //获取最小值
  2. func GetMin(array []int) int {
  3. index := 0
  4. min := array[0]
  5. for k, value := range array {
  6. if min > value && value != 0 {
  7. min = value
  8. index = k
  9. }
  10. }
  11. array[index] = 0
  12. return min
  13. }

欢迎大家关注微信公众号:“golang那点事”,更多精彩期待你的到来
GoLang公众号.jpg