归并排序介绍、详解、案例

9 篇文章 0 订阅

排序

归并排序也是基于分治法的排序算法,为了排序长度为n的数组,需要先排序长度为n/2的字数组,然后合并这两个排序字数组于是整个数组也就排序完毕。

排序过程

在这里插入图片描述

以数组[4,1,5,6,2,7,8,3]为例子,进行排序说明。如上图所示:

  1. 首先排序合并相邻为1的字数组,得到四个排序长度为2的字数组[1,4,5,6,2,7,3,8]。
  2. 接着排序合并相邻长度为2的字数组,得到两个排序长度为4的字数组[1,4,5,6,2,3,7,8]。
  3. 最后排序合并相邻长度为4的字数组,得到最终的排序结果[1,2,3,4,5,6,7,8]。

归并排序需要创建一个和原数组相同大小的数组,用来保存排序之后的结果,所以其空间复杂度为O(n)。

代码表示

fun mergeIntoSort(array: IntArray): IntArray {
    val length = array.size
    var src = array
    var dst = IntArray(length)
    //合并相邻序列的数组,从1开始
    var sequence = 1
    while (sequence < length) {
        //循环数组 按照序列进行 子数组排序
        var start = 0
        while (start < length) {
            val middle = min(sequence + start, length)
            val end = min(sequence * 2 + start, length)
            var i = start
            var j = middle
            var k = start
            //字数组 具体的排序过程
            while (i < middle || j < end) {
                if (j == end || (i < middle && src[i] < src[j])) {
                    dst[k++] = src[i++]
                } else {
                    dst[k++] = src[j++]
                }
            }
            start += sequence * 2
        }
        //交换数组 主要是防止真正的排序是 dst和src指向同一个数组
        val temp = src
        src = dst
        dst = temp
        //下一个序列
        sequence += sequence
    }
    return src
}

时间复杂度:长度为n的数组每次都被拆分为n/2,所以归并排序的时间复杂度为O(nlogn)。

空间复杂度:需要长度为n的额外辅助数组,所以归并排序的空间复杂度为O(n)。

为什么快速排序比归并排序要快

从上述的时间复杂度我们可以看出,归并时间复杂度固定为O(nlogn),快排的平均时间复杂度才为O(nlogn)。为什么大家常用的是快速排序而不是归并排序呢?

主要是因为归并排序的不是原地排序算法。归并排序的合并过程需要对多个数组进行操作,而快速排序只需要进行简单的元素比较和交换操作。这意味着归并排序的常数复杂度可能比快速排序高。所以大部分情况下快排会更快,且不需要额外的空间。

示例:链表排序

输入一个链表的头节点,请将该链表排序。

例如输入的是:[3 -> 5 -> 1 -> 4 -> 2 -> 6] 输出的则是:[1 -> 2 -> 3 -> 4 -> 5 -> 6]。

如何实现

快排?快速排序算法首先随机生成一个下标,并以该下标对应的值作为中间值进行分区。如果输入的是数组,那么只需要O(1)的时间就能根据下标找到一个数字。但如果输入的是链表,那么需要O(n)的时间才能根据节点的编号找到对应的节点。

归并?归并排序的主要思想是将链表分成两个子链表,在对两个子链表排序后再将它们合并成一个排序的链表。所以可行

如下所示:

fun sortListNodeByMergeInto(node: ListNode?): ListNode? {
    if (node == null || node.next == null) {
        return node
    }

    val middleNode = findMiddleNodeAndSplit(node)
    val node1 = sortListNodeByMergeInto(node)
    val node2 = sortListNodeByMergeInto(middleNode)

    return merge(node1, node2)
}

上述代码中的函数findMiddleNodeAndSplit将链表分成两半并返回后半部分链表的头节点。再将链表分成两半后分别递归地将它们排序,然后调用函数merge将它们合并起来。接下来讨论函数findMiddleNodeAndSplitmerge的实现细节。

findMiddleNodeAndSplit用快慢双指针的思路将链表分成两半。如果慢指针一次走一步,快指针一次走两步,当快指针走到链表尾部时,慢指针只走到链表的中央,这样也就找到了链表后半部分的头节点。

fun findMiddleNodeAndSplit(node: ListNode): ListNode? {
    var slowPointer: ListNode? = node
    var quickPointer: ListNode? = node.next
    while (quickPointer != null && quickPointer.next != null) {
        slowPointer = slowPointer?.next
        quickPointer = quickPointer.next!!.next
    }

    val second = slowPointer?.next
    slowPointer?.next = null

    return second
}

merge也可以用两个指针分别指向两个排序子链表的节点,然后每次选择其中值较小的节点。与合并数组不同的是,不需要另外一个链表来保存合并之后的节点,而只需要调整指针的指向。

fun merge(node1: ListNode?, node2: ListNode?): ListNode? {
    val dummy = ListNode()
    var newNode: ListNode? = dummy
    var leftNode = node1
    var rightNode = node2
    while (leftNode != null && rightNode != null) {
        if (leftNode.value < rightNode.value) {
            newNode!!.next = leftNode
            leftNode = leftNode.next
        } else {
            newNode!!.next = rightNode
            rightNode = rightNode.next
        }
        newNode = newNode.next
    }
    newNode!!.next = leftNode ?: rightNode
    return dummy.next
}
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pumpkin的玄学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值