归并排序
核心思想:如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了
复制代码
func MergeSort(arr []int) {
arrLen := len(arr)
if arrLen <= 1 {
return
}
mergeSort(arr, 0, arrLen-1)
}
func mergeSort(arr []int, start, end int) {
//终止条件,如果开始下标大于等于 结束下标,说明已不可再分
if start >= end {
return
}
mid := (start + end) / 2
//排序前半部分
mergeSort(arr, start, mid)
//排序后半部分
mergeSort(arr, mid+1, end)
//将排序好的两部分进行合并排序
merge(arr, start, mid, end)
}
func merge(arr []int, start, mid, end) {
s := make([]int, end-start+1)
//遍历前一部分
i := start
//遍历后一部分
j := mid + 1
//新切片下标
k := 0
for ; i <= mid && j <= end; k++ {
if arr[i] < arr[j] { //因为i和mid之间已经是有有序的了,所以要判断i和j的大小。
s[k] = arr[i]
i++
} else {
s[k] = arr[j]
j++
}
}
//如果有一个未遍历完,将其剩余元素加入到新切片s中
for i <= mid {
s[k] = arr[i]
i++
k++
}
for j <= end {
s[k] = arr[j]
j++
k++
}
//将新排序好的切片,copy到原切片arr中,保证strat-end已排好序
copy(arr[start:end+1], s)
}
复制代码
第一,归并排序是稳定的排序算法吗
复制代码
- 结合我前面画的那张图和归并排序的伪代码,你应该能发现,归并排序稳不稳定关键要看 merge() 函数,也就是两个有序子数组合并成一个有序数组的那部分代码。
- 在合并的过程中,如果 A[p…q]和 A[q+1…r]之间有值相同的元素,那我们可以像伪代码中那样,先把 A[p…q]中的元素放入 tmp 数组。这样就保证了值相同的元素,在合并前后的先后顺序不变。所以,归并排序是一个稳定的排序算法。
第二,归并排序的时间复杂度是多少?
复制代码
- 归并排序的时间复杂度是 O(nlogn)。
- 从我们的原理分析和伪代码可以看出,归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。
第三,归并排序的空间复杂度是多少?
复制代码
- 归并排序的时间复杂度任何情况下都是O(nlogn)。看起来非常优秀(即使是快速排序,最坏情况下,时间复杂度也是O(n2))但是,归并排序并没有像快排那样,应用广泛这是为什么呢?因为它有一个致命的"缺点",那就是归并排序不是原地排序
- 这是因为归并排序的合并函数,在合并两个有序数组为一个有序数组时,需要借助额外的存储空间。空间复杂度为O(nlogn)
快速排序
核心思想:如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。
根据分治、递归的处理思想,我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到 r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。
复制代码
代码:
func QuickSort(arr []int) {
separateSort(arr, 0, len(arr)-1)
}
func separateSort(arr []int, start, end int) {
if start >= end {
return
}
//i的位置为pivot
i := partition(arr, start, end)
//将i元素左边就行递归
separateSort(arr, start, i-1)
//将i元素右边进行递归
separateSort(arr, i+1, end)
}
func partition(arr []int, start, end int) int {
//挑选最后一个元素为pivot
pivot := arr[end]
//i元素的左边都是已排序区间,i元素始终指向第一个未排序空间的元素,即大于pivot的元素
i := start
for j:= start; j < end; j++ { //没有j<=end 的原因是pivot 已经==end
if arr[j] < pivot {
// i和j相等说明指向的同一个元素
if !(i==j) {
arr[i], arr[j] = arr[j], arr[i]
}
//只有 j遍历的元素小于了pivot,i才能++,因为i的左边都是小于pivot的元素
i ++
}
}
//j < end 所以遍历完之后j所指的元素就是最后一个元素 (将最后一个元素与当前i交换,因为i指向的是未排序元素的第一个)
arr[i], arr[end] = arr[end], arr[i]
//此时的i的位置为 pivot,上一步进行了交换
return i
}
复制代码
-
空间复杂度O(1)
-
平均复杂度O(nlogn)
-
不稳定排序
-
归并排序和快速排序是两种稍微复杂的排序算法,它们用的都是分治的思想,代码都通过递归来实现,过程非常相似。理解归并排序的重点是理解递推公式和 merge() 合并函数。同理,理解快排的重点也是理解递推公式,还有 partition() 分区函数。归并排序算法是一种在任何情况下时间复杂度都比较稳定的排序算法,这也使它存在致命的缺点,即归并排序不是原地排序算法,空间复杂度比较高,是 O(n)。正因为此,它也没有快排应用广泛。