冒泡排序
思路:从第一个元素开始往后比较,如果遇到比当前数大的数,就交换,直到数组末尾,此时末尾就是数组最大元素,然后重复此操作,只是比较到上一个末尾的前一个元素即可
func BubbleSort(sli []int) {
if sli == nil || len(sli) < 2 {
return
}
for i := 0; i < len(sli)-1; i++ {
for j := 0; j < len(sli)-i-1; j++ {
if sli[j] > sli[j+1] {
sli[j], sli[j+1] = sli[j+1], sli[j]
}
}
}
}
插入排序
思路:从最左边第一个元素开始,此时只有一个元素,所以看作是有序的,然后查看下一个元素,在前面已经有序的数中找到自己应该在的位置,循环此操作。
func InsertionSort(sli []int) {
if sli == nil || len(sli) < 2 {
return
}
for i := 0; i < len(sli)-1; i++ {
for j := i + 1; j > 0; j-- {
if sli[j] > sli[j-1] {
break
}
sli[j], sli[j-1] = sli[j-1], sli[j]
}
}
}
选择排序
思路:i = 0 ,从 i 开始遍历数组找到最小数的 index,将其放在 i 位置,i++,循环此操作
func SelectionSort(sli []int) {
if sli == nil || len(sli) < 2 {
return
}
for i := 0; i < len(sli)-1; i++ {
min := i
for j := i + 1; j < len(sli); j++ {
if sli[min] > sli[j] {
sli[min], sli[j] = sli[j], sli[min]
}
}
}
}
归并排序
思路:将数组分为左边与右边两半部分,使其各自有序,然后做归并操作。
归并:新建一个 help 数组与原数组等大小,使用两个指针 p1,p2 分别指向原数组左右两边起始位置,按照一定规则比较 p1 p2,假设 p1 在比较中胜出,将 p1 指向的元素放入 help 数组,p1 向右移动,再次进行比较,放入 help 数组的过程,直到 p1 或 p2 越界,然后将未越界一方的剩余元素放入 help 中,将 help 拷贝到原数组中。
func MergeSort(sli []int) {
if sli == nil || len(sli) < 2 {
return
}
process(sli, 0, len(sli)-1)
}
func process(arr []int, l, r int) {
if l == r {
return
}
mid := l + (r-l)>>1
process(arr, l, mid)
process(arr, mid+1, r)
merge(arr, l, r, mid)
}
func merge(arr []int, l, r, mid int) {
help := make([]int, r-l+1)
i, p1, p2 := 0, l, mid+1
for p1 <= mid && p2 <= r {
if arr[p1] < arr[p2] {
help[i] = arr[p1]
p1++
} else {
help[i] = arr[p2]
p2++
}
i++
}
for p1 <= mid {
help[i] = arr[p1]
p1++
i++
}
for p2 <= r {
help[i] = arr[p2]
p2++
i++
}
i = 0
for l <= r {
arr[l] = help[i]
i++
l++
}
}
快速排序
思路:每次选定一个 pivot,使数组左边全部小于 pivot,右边全部大于 pivot,中间为等于 pivot,然后在得到的左边和右边重复此操作,直到数组有序
// QuickSort 3.0 版本,每次随机选取一个数作为基准
func QuickSort(sli []int) {
quickSort(sli, 0, len(sli)-1)
}
func quickSort(arr []int, l, r int) {
if l < r {
rand.Seed(time.Now().UnixNano())
randIndex := rand.Intn(r-l+1) + l
arr[randIndex], arr[r] = arr[r], arr[randIndex]
p := partition(arr, l, r)
quickSort(arr, l, p[0]-1)
quickSort(arr, p[1]+1, r)
}
}
// 返回小于区的右边界和大于区的左边界
func partition(arr []int, l, r int) []int {
less, more := l-1, r
for l < more {
if arr[l] < arr[r] {
arr[l], arr[less+1] = arr[less+1], arr[l]
less++
l++
} else if arr[l] > arr[r] {
arr[l], arr[more-1] = arr[more-1], arr[l]
more--
} else {
l++
}
}
arr[r], arr[more] = arr[more], arr[r]
return []int{less + 1, more}
}
堆排序
思路:将数组转化为大根堆,将堆顶与堆尾交换,然后 heapSize--,重复此操作直到 heapSize减小到1
func HeapSort(sli []int) {
if sli == nil || len(sli) < 2 {
return
}
for heapSize := heap.ConvertArrToHeap(sli, 0, len(sli)-1, heap.MaxHeapComparator()); heapSize > 1; {
sli[0], sli[heapSize-1] = sli[heapSize-1], sli[0]
heapSize--
heap.Heapify(sli, 0, heapSize, heap.MaxHeapComparator())
}
}
计数排序
计数排序适用与数组的数在 [left,right] 之间,且 right - left 不大
思路:创建一个 count 数组,大小为 right - left + 1,遍历原数组,每次都执行 count[array[i]-left]++ 的操作,这样就得到了原数组的词频统计表 count,再次遍历count,将 count 的每一个元素都输出 count[i] 次,每一个元素的值为 i+left
func CountingSort(sli, r []int) {
if sli == nil || len(sli) < 2 || len(r) < 2 {
return
}
left, right := r[0], r[1]
count := make([]int, right-left+1)
for i := 0; i < len(sli); i++ {
count[sli[i]-left]++
}
index := 0
for i := 0; i < len(count); i++ {
for j := count[i]; j > 0; j-- {
sli[index] = i + left
index++
}
}
}
基数排序
思路:先创建十个桶,编号为 0-9,遍历数组,每次查看元素某一位上的数(个位十位百位...),将其放入对应编号的桶中,然后将桶中的元素按编号从左往右依次倒出(先入先出),只需要执行数组中元素最多位数次即可完成排序
这里使用的是词频表法实现基数排序
func RadixSort(sli []int) {
if sli == nil || len(sli) < 2 {
return
}
digit := maxBits(sli)
bucket := make([]int, len(sli))
for d := 1; d <= digit; d++ {
count := make([]int, 10)
for _, v := range sli {
count[getDigit(v, d)]++
}
for i := 1; i < 10; i++ {
count[i] += count[i-1]
}
for i := len(sli) - 1; i >= 0; i-- {
j := getDigit(sli[i], d)
bucket[count[j]-1] = sli[i]
count[j]--
}
for i, _ := range sli {
sli[i] = bucket[i]
}
}
}
func getDigit(x, d int) (digit int) {
return (x / int(math.Pow10(d-1))) % 10
}
func maxBits(arr []int) (result int) {
max := 0
for _, v := range arr {
if v > max {
max = v
}
}
for max != 0 {
result++
max /= 10
}
return
}
复杂度
| type | 时间复杂度 | 空间复杂度 | 是否稳定 |
|---|---|---|---|
| 冒泡排序 | o(N^2) | O(1) | 是 |
| 插入排序 | O(N^2) | O(1) | 是 |
| 选择排序 | O(N^2) | O(1) | 否 |
| 归并排序 | O(N*logN) | O(N) | 是 |
| 快速排序 | O(N*logN) | O(logN) | 否 |
| 堆排序 | O(N*logN) | O(1) | 否 |
| 计数排序 | O(N+K) | O(N) | 是 |
| 基数排序 | O(dN) | 是 |
常见问题
- 归并排序内部缓存法可以将空间复杂度降为 O(1),但是比较难
- ”原地归并排序“会让时间复杂度降为 O(N^2) ,且比较难,完全没必要参考
- 快速排序可以做到稳定,但是比较难,参考 "01 stable sort"
- 目前没有找到时间复杂度为 O(N*logN),额外空间复杂度为 O(1) ,又稳定的算法
排序算法的选择
我们应该根据数据的状况,对稳定性的要求等方面考虑选择排序算法。
如我们排序的不是基本数字类型,而是 Student 类型,我们就应该选择一种稳定的排序算法,如果排序的数据量并不大,我们可以直接选择时间复杂度为 O(N^2) 的排序算法,再比如我们可以在快速排序递归到较小数据量时,我们可以使用选择排序来提高算法效率(n < 60)。
排序算法的应用
起始排序算法不止是可以应用在排序上,我们也可以应用其算法的实现过程来实现很多东西,如归并排序的归并过程,快速排序的 partition 过程都是值得我们学习的,以上列出的排序算法都是值得我们学习并掌握的。