排序
- 计数排序介绍、详解、案例
- 快速排序介绍、详解、案例
- 归并排序介绍、详解、案例
归并排序也是基于分治法的排序算法,为了排序长度为n的数组,需要先排序长度为n/2的字数组,然后合并这两个排序字数组于是整个数组也就排序完毕。
排序过程
以数组[4,1,5,6,2,7,8,3]为例子,进行排序说明。如上图所示:
- 首先排序合并相邻为1的字数组,得到四个排序长度为2的字数组[1,4,5,6,2,7,3,8]。
- 接着排序合并相邻长度为2的字数组,得到两个排序长度为4的字数组[1,4,5,6,2,3,7,8]。
- 最后排序合并相邻长度为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
将它们合并起来。接下来讨论函数findMiddleNodeAndSplit
和merge
的实现细节。
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
}