当前位置: 首页 > news >正文

Go语言实现十大排序算法超细节图片讲解

基础排序

冒泡排序

        将序列中的元素进行两两比较,将大的元素移动到序列的末尾。

        平均时间复杂度是O(n^2),最坏时间复杂度是O(n^2),最好时间复杂度是O(n),排序结果具有稳定性,空间复杂度是O(1)。

        这里所说的稳定性是针对相同元素而言的,比如5,5,3进行冒泡排序,第一个5由于并不大于第二个5,所以两个5的位置不会发生交换,如果排序的时候,两个5的位置发生了交换我们就说,这种排序是不稳定的。

func BubleSort(nums []int) {n := len(nums)finish := falsefor i := 0; i < n; i++ {for j := 0; j < n-1-i; j++ {if nums[j] > nums[j+1] {nums[j], nums[j+1] = nums[j+1], nums[j]finish = true}}if !finish {break}finish = false}
}

选择排序

        假设序列中的第一个元素为最小值,然后遍历后续的元素找到真正的最小值,每次在剩余元素中选择最小的元素与当前元素进行交换。

        平均时间复杂度是O(n^2),最好时间复杂度是O(n^2),最坏时间复杂度是O(n^2),排序结果不稳定,空间复杂度是O(1)。使用选择排序对5,5,3进行排序,会将第一个5和最后一个3进行交换,这样两个5的前后顺序就变了,所以选择排序是不稳定的。

func ChoiceSort(nums []int) {n := len(nums)for i := 0; i < n-1; i++ {minIndex := ifor j := i + 1; j < n; j++ {if nums[j] < nums[minIndex] {minIndex = j}}nums[i], nums[minIndex] = nums[minIndex], nums[i]}
}

插入排序

        从第二个元素开始,把前面的元素当作有序的,然后在这些元素中找到小于等于当前元素的元素,在这个元素的后面插入当前元素。

        最坏时间复杂度和平均时间复杂度都是O(n^2),最好时间复杂度是O(n),排序结果稳定,对于趋于有序的数据,具有最高效率,空间复杂度是O(1)。

func InsertSort(nums []int) {n := len(nums)for i := 1; i < n; i++ {num := nums[i]j := i - 1for ; j >= 0; j-- {nums[j+1] = nums[j]if nums[j] < num {break}}nums[j+1] = num}
}

高级排序

希尔排序

        对插入排序的优化,其实就是多路的插入排序,插入排序本身只有一个分组,而希尔排序通过设置分组间隔的方式可以产生多个分组,对这些分组进行插入排序,分组的数据越趋于有序,则整体的数据越趋于有序。

        最坏时间复杂度是O(n^2),最好时间复杂度是O(n),平均时间复杂度是O(n^1.3),排序结果稳定,空间复杂度是O(1)。

func ShellSort(nums []int) {n := len(nums)for gap := n / 2; gap != 0; gap /= 2 {for i := gap; i < n; i++ {num := nums[i]j := i - gapfor ; j >= 0; j -= gap {nums[j+gap] = nums[j]if nums[j] < num {break}}nums[j+gap] = num}}
}

        对比代码可以发现,希尔排序其实就是将插入排序的1换成了分组间隔gap。

快速排序

        选取一个基准数,把小于基准数的元素放到基准数的左边,将大于基准数的元素放到基准数的右边,然后以基准数作为划分,将序列划分为左右两个序列,再次将元素与基准数进行比对和移动,直到整个序列变为有序。
        快排一般使用递归实现,先选取基准数,一般就是序列的最左边的元素,然后从右边开始找第一个比基准数小的元素,令此时遍历到的左边的元素等于该元素,并且左索引向后移动1,再从左边寻找第一个比基准数大的元素,令此时遍历到的右边的元素等于该元素,右索引向前移动1,重复以上操作,直到序列有序。
        快排的最好和平均时间复杂度是O(nlogn),最坏时间复杂度是O(n^2),空间复杂度在O(logn)到O(n)之间,排序结果不稳定,一般来说序列越乱序,快排效率越高,与插入排序相反。

func QuickSort(nums []int, left int, right int) {if left >= right {return}num := nums[left]i := leftj := rightfor i < j {for i < j && nums[j] >= num {j--}if i < j {nums[i] = nums[j]i++}for i < j && nums[i] <= num {i++}if i < j {nums[j] = nums[i]j--}}nums[i] = numQuickSort(nums, left, i-1)QuickSort(nums, i+1, right)
}

        实际上快速排序的效率取决于左右序列的划分,也就是基准数的选取,要保证划分的序列可以组成一个平衡的二叉树,这时排序的效率通常是最好的,可以通过三数取中法的方式有效选取较好的基准数,每次较好地划分左右序列,当然也可以使用快排加插入排序结合的方式,提高快排的效率,毕竟快速排序的左右序列都是趋于有序的,适用于插入排序。

归并排序

        先递进(可以用二分递进)到序列的各个元素,然后回归将各个元素的顺序进行编排,并将编排好的左右序列进行合并,最终使得整个序列变得有序。

        归并排序的最好时间复杂度和最坏时间复杂度以及平均时间复杂度都是O(nlogn),因为归并排序无论序列是否有序,它都会进行二分递归将序列分为单个元素,再合并为有序的序列,所以没有最好和最坏时间复杂度的区别,空间复杂度是O(n+logn),也就是O(n),归并排序是稳定排序。

func merge(nums []int, left int, mid int, right int) {//开辟临时数组用于存放排好序的序列order_res := make([]int, right-left+1)i := leftj := mid + 1index := 0for i <= mid && j <= right {if nums[i] < nums[j] {order_res[index] = nums[i]index++i++} else {order_res[index] = nums[j]index++j++}}//左边序列还有剩余的元素for i <= mid {order_res[index] = nums[i]index++i++}//右边序列还有剩余的元素for j <= right {order_res[index] = nums[j]index++j++}//将排好序的序列复制到原数组中for i, j := left, 0; i <= right; i++ {nums[i] = order_res[j]j++}
}
func MergeSort(nums []int, left int, right int) {if left >= right {return}mid := (left + right) / 2//先递进到单个元素,单个元素本身就是有序的MergeSort(nums, left, mid)MergeSort(nums, mid+1, right)//然后将两个有序的子序列合并成一个有序的序列merge(nums, left, mid, right)
}

        归并排序有自己独特的优势,那就是它是八大排序算法中唯一的外排序,其他排序都是内排序,只能对内存上的数据进行排序,但是归并排序可以对磁盘里面的数据进行排序,比如我D盘里面一个test.txt中有1024MB的数据序列,但是我的内存只有100MB,这时候只有归并排序的思想可以在使用100MB内存的情况下,完成磁盘上1024MB数据的排序,实际上就是将test.txt中的数据序列分为多个文件存储,比如test1.txt、test2.txt等,这些.txt文件中只有100MB的文件序列,然后分别对它们进行归并排序,最后从这些已经排好序的文件中不断选取最小值,组成一个有序的1024MB的序列,这就是归并排序的先分再合的思想。

堆排序

        堆排序实际上是基于二叉堆进行的,而二叉堆就是一个数组,只是我们从逻辑的角度将这个给数组看作一颗完全二叉树,这个二叉树就是二叉堆,二叉堆又分为大根堆和小根堆,堆排序如果是基于大根堆实现的就是,从小到大排序,小根堆就是从大到小排序。

        进行堆排序的第一步就是从第一个非叶子节点开始到堆顶元素的每一个节点都进行下沉操作,将本来的二叉堆调整为大根堆,第一个非叶子节点的数组索引可以通过数组末尾元素的索引n得到,(n-1)/2。接着把堆顶元素和末尾元素进行交换,从0号位继续开始进行堆的下沉操作。实际上就是将每一个有孩子的节点所对应的子树都调整为大根堆。

        这里的下沉操作的规则是,如果左右节点都大于当前节点,那么优先选择大的子节点上浮。

        堆排序的平均时间复杂度和最好最坏时间复杂度都是nlogn,空间复杂度是O(1),排序的结果不稳定。

func siftDown(nums []int, n, i int) {for {l := 2*i + 1r := 2*i + 2max := iif l < n && nums[l] > nums[max] {max = l}if r < n && nums[r] > nums[max] {max = r}if max == i {break}nums[i], nums[max] = nums[max], nums[i]i = max}
}func HeapSort(nums []int) {n := len(nums)//从n/2-1开始构建一颗大根堆完全二叉树for i := n/2 - 1; i >= 0; i-- {siftDown(nums, n, i) //从n/2-1}//从0号元素开始,将根节点与最后一个元素交换,然后对剩下的元素进行下沉操作,调整为新的大根堆for i := n - 1; i > 0; i-- {nums[0], nums[i] = nums[i], nums[0]siftDown(nums, i, 0) //传入i,而不是n,是因为只对剩下的元素进行下沉操作,以及下沉到底的最大元素不再下沉}
}

         需要注意的是,尽管快排和归并排序和堆排序,它们的时间复杂度基本相差不大,但是实际使用进行排序的时候,堆排序的效率要比快排和归并排序差的多主要是因为快排和归并排序是顺序遍历数组元素的,我们知道,CPU在执行一段指令或者使用一段数据的时候,它不是一条一条从内存中获取的,而是将一块相邻的内存都加载到自己的空间中,如果是当前要使用的数据或者要执行的指令就放入到CPU寄存器中,如果暂时不需要使用的数据和不执行的指令则放入到缓存中,但是所有的指令和数据都是相邻一块内存中的,所以快排和归并排的机制对于CPU的缓存使用相当友好,可以提高CPU的缓存命中率,快排和归并派使用到的数据大部分都是从缓存中拿取的,而不是从进程的内存中拿取的,自然效率要更高,而堆排序就不一样了,它访问元素是通过上浮和下沉操作,并不是顺序访问元素,因此更多是从内存取数据,因此效率要低的多,而且堆排序在下沉操作中的无效交换次数较多,因为每次进行下沉的时候,都会直接将数组末尾元素也就是堆尾部的元素直接覆盖堆顶部的元素,然后再对堆顶元素进行下沉,本身数组末尾元素在二叉堆里面的属性就比较极端,如果是在大根堆,那这个元素极有可能是最小的,这时候直接放到堆顶,后面又要下沉回来,因此极其浪费时间。

桶排序

        按照数据的某种性质将一组数组序列映射分配到数量有限的桶里面,每个桶再分别排序,然后将每个桶里面排序好序的序列重新组合为一个新的有序序列。

        这里说的某种性质,就是例如数据的大小范围、分布情况等,如果我们知道数据的范围在[0,1)之间,我们可以将数据乘以桶的数量来映射到不同的桶中;另外也可以根据数据的分布情况,将数据按照一定的映射规则划分到桶中。

        桶排序在对每个桶中的元素进行排序时,通常依赖于其他的排序算法,比如快排或者插入排序等,因此桶排序的时间复杂度也取决于所使用到的排序算法的时间复杂度,一般来说,如果使用Go标准库自带的sort函数对各个桶里面的元素进行排序,桶排序的最好时间复杂度是O(n),平均时间复杂度也是O(n+k),最坏时间复杂度是O(n^2),空间复杂度是O(n+k),需要k个桶和n个元素的额外空间,排序结果稳定。

func BucketSort(nums []float64) {n := len(nums)//创建桶var bucket [][]float64 = make([][]float64, n)//创建桶的索引var index []int = make([]int, n)//计算机桶的索引for i := 0; i < n; i++ {index[i] = int(nums[i] * float64(n))}//将元素放入桶中for i := 0; i < n; i++ {bucket[index[i]] = append(bucket[index[i]], nums[i])}//对桶的每一行元素进行排序for i := 0; i < n; i++ {sort.Float64s(bucket[i])}//将桶中的元素放入原数组中for i, q := 0, 0; i < n; i++ {for j := 0; j < len(bucket[i]); j++ {nums[q] = bucket[i][j]q++}}
}

        需要注意的是,桶排序适用于均匀分布的数据,主要用于特定范围内(小于1大于0)的浮点数排序,按照上面的映射方式,如果是对整数进行排序,数据产生的映射下标,直接就超出桶的大小了,这时需要转换映射关系。

计数排序

        由桶排序衍生出来的排序,同样是根据数据的某种性质将数据分配到不同的桶中,然后进行排序,但是它是根据原始数据序列中最小元素和最大元素的值开辟桶的大小,那么这个桶实际上是一个一维的数组,并且的索引的范围是从0到原始数据序列中的最大值,将原始数据序列中每一个元素对应在桶里的位置都+1,原始数据序列中同一个元素出现几次,对应桶中这个元素索引的值就是几,比如我原始序列是{1,2,5,9,1,3,8,10},显然桶的大小是10,其中桶的第0号索引对应的值显然是0,因为0没有出现在原始数据序列中,第1号索引对应的值自然就是2,因为1在原始序列中出现了2两次,就是按照这种方式,将原始序列映射到了桶中,然后从桶的0索引开始依次将所有元素取出放入元素序列中,这样就能达到一个排序的效果。

        计数排序从机制就可以看出来,非常的损耗空间,假如我上面那个序列的1改为99,那么就要开辟一个99大小的桶数组出来,计数排序的平均时间复杂度和最好最坏时间复杂度都是O(n+k),空间复杂度是O(k),排序结果稳定。

       假设原始序列如下:

        计数排序过程如下:

func CountingSort(nums []int) {n := len(nums)//找出最大值max := nums[0]for i := 1; i < n; i++ {if nums[i] > max {max = nums[i]}}//使用最大值作为桶的长度var bucket []int = make([]int, max+1)//使用元素作为索引,元素出现一次,索引对应的值+1for i := 0; i < n; i++ {bucket[nums[i]]++}//将桶中的元素放入原数组中for i, q := 0, 0; i < max+1; i++ {for ; bucket[i] > 0; bucket[i]-- {nums[q] = iq++}}
}

基数排序

        同样是桶排序衍生出来的排序,首先找到整个原始序列中位数最长的数字,确定桶排序的趟数,第一次将原始序列中的数据按照个位数字,依次放入到0~19的20个桶中进行分类,然后依次从这个0~19个桶中将数组拿出重新拼凑为一个新的序列,再按照十位的数字进行同样的操作,直到完成所有的趟数。

        基数排序的最坏时间复杂度和平均时间复杂度以及最好时间复杂度都是O(n*k),空间复杂度是O(n),排序结果是稳定的。

func RaixSort(nums []int) {n := len(nums)max := nums[0]//找出最大值的绝对值,计算排序的轮数abs := func(x int) int {return -x}for i := 0; i < n; i++ {if abs(nums[i]) > max {max = abs(nums[i])}}turns := len(strconv.Itoa(max))mod := 10 //计算余数dev := 1  //计算除数for i := 0; i < turns; i++ {//创建桶,桶的数量是固定的,20个,可以让索引覆盖-9~9bucket := make([][]int, 20)//根据当前元素的第i位数作为索引,将元素依次放入桶中for j := 0; j < n; j++ {index := nums[j]/dev%mod + 10 //+10是为了处理负数,负数不能作为索引bucket[index] = append(bucket[index], nums[j])}//将桶中的元素依次放入到原数组中index := 0for w := 0; w < 20; w++ {for q := 0; q < len(bucket[w]); q++ {nums[index] = bucket[w][q]index++}}//增加位数mod *= 10dev *= 10}
}

        需要注意的是,这里选用20个桶,是为了基数排序可以处理负数排序,用20个桶就可以容下-9~9这20个数字,除此之外,从基数排序的位数处理方式来看,就可以很明显看出,基数排序不适合浮点数排序。 

测试程序

        用随机数进行测试。

package mainimport ("fmt""math/rand""sort""strconv""time"
)func BubleSort(nums []int) {n := len(nums)for i := 0; i < n-1; i++ {finish := falsefor j := 0; j < n-1-i; j++ {if nums[j] > nums[j+1] {nums[j], nums[j+1] = nums[j+1], nums[j]finish = true}}if !finish {break}finish = false}
}
func ChoiceSort(nums []int) {n := len(nums)for i := 0; i < n; i++ {min := ifor j := i + 1; j < n; j++ {if nums[j] < nums[min] {min = j}}nums[i], nums[min] = nums[min], nums[i]}
}
func InsertSort(nums []int) {n := len(nums)for i := 1; i < n; i++ {num := nums[i]j := i - 1for ; j >= 0; j-- {nums[j+1] = nums[j]if nums[j] <= num {break}}nums[j+1] = num}
}
func ShellSort(nums []int) {n := len(nums)for gap := n / 2; gap != 0; gap /= 2 {for i := gap; i < n; i++ {num := nums[i]j := i - gapfor ; j >= 0; j -= gap {nums[j+gap] = nums[j]if nums[j] <= num {break}}nums[j+gap] = num}}
}
func QuickSort(nums []int, left int, right int) {if left >= right {return}i := leftj := rightmidNum := nums[left]for i != j {for i != j && nums[j] >= midNum {j--}if i != j {nums[i] = nums[j]i++}for i != j && nums[i] <= midNum {i++}if i != j {nums[j] = nums[i]j--}}nums[i] = midNumQuickSort(nums, left, i-1)QuickSort(nums, i+1, right)
}
func merge(nums []int, left int, mid int, right int) {//开辟临时数组用于存放排好序的序列order_res := make([]int, right-left+1)i := leftj := mid + 1index := 0for i <= mid && j <= right {if nums[i] < nums[j] {order_res[index] = nums[i]index++i++} else {order_res[index] = nums[j]index++j++}}//左边序列还有剩余的元素for i <= mid {order_res[index] = nums[i]index++i++}//右边序列还有剩余的元素for j <= right {order_res[index] = nums[j]index++j++}//将排好序的序列复制到原数组中for i, j := left, 0; i <= right; i++ {nums[i] = order_res[j]j++}
}
func MergeSort(nums []int, left int, right int) {if left >= right {return}mid := (left + right) / 2//先递进到单个元素,单个元素本身就是有序的MergeSort(nums, left, mid)MergeSort(nums, mid+1, right)//然后将两个有序的子序列合并成一个有序的序列merge(nums, left, mid, right)
}
func siftDown(nums []int, n, i int) {for {l := 2*i + 1r := 2*i + 2max := iif l < n && nums[l] > nums[max] {max = l}if r < n && nums[r] > nums[max] {max = r}if max == i {break}nums[i], nums[max] = nums[max], nums[i]i = max}
}func HeapSort(nums []int) {n := len(nums)//从n/2-1开始构建一颗大根堆完全二叉树for i := n/2 - 1; i >= 0; i-- {siftDown(nums, n, i) //从n/2-1}//从0号元素开始,将根节点与最后一个元素交换,然后对剩下的元素进行下沉操作,调整为新的大根堆for i := n - 1; i > 0; i-- {nums[0], nums[i] = nums[i], nums[0]siftDown(nums, i, 0) //传入i,而不是n,是因为只对剩下的元素进行下沉操作,以及下沉到底的最大元素不再下沉}
}
func BucketSort(nums []float64) {n := len(nums)//创建桶var bucket [][]float64 = make([][]float64, n)//创建桶的索引var index []int = make([]int, n)//计算机桶的索引for i := 0; i < n; i++ {index[i] = int(nums[i] * float64(n))}//将元素放入桶中for i := 0; i < n; i++ {bucket[index[i]] = append(bucket[index[i]], nums[i])}//对桶的每一行元素进行排序for i := 0; i < n; i++ {sort.Float64s(bucket[i])}//将桶中的元素放入原数组中for i, q := 0, 0; i < n; i++ {for j := 0; j < len(bucket[i]); j++ {nums[q] = bucket[i][j]q++}}
}
func CountingSort(nums []int) {n := len(nums)//找出最大值max := nums[0]for i := 1; i < n; i++ {if nums[i] > max {max = nums[i]}}//使用最大值作为桶的长度var bucket []int = make([]int, max+1)//使用元素作为索引,元素出现一次,索引对应的值+1for i := 0; i < n; i++ {bucket[nums[i]]++}//将桶中的元素放入原数组中for i, q := 0, 0; i < max+1; i++ {for ; bucket[i] > 0; bucket[i]-- {nums[q] = iq++}}
}
func RadixSort(nums []int) {//找出最大值,并计算出最大值的位数,作为排序的轮数n := len(nums)maxElement := nums[0]for i := 1; i < n; i++ {abs_num := func(num int) int {if num < 0 {return -num}return num}(nums[i])if abs_num > maxElement {maxElement = abs_num}}turns := len(strconv.Itoa(maxElement))mod := 10 //用于计算余数dev := 1  //用于计算除数for i := 0; i < turns; i++ {//创建桶,桶的数量是固定的,20个,可以让索引覆盖-9~9var bucket [][]int = make([][]int, 20)//根据当前元素的第i位数作为索引,将元素依次放入桶中for j := 0; j < n; j++ {index := nums[j]/dev%mod + 10 //+10是为了处理负数bucket[index] = append(bucket[index], nums[j])}//将桶中的元素依次放入到原数组中for w, x := 0, 0; w < 20; w++ {for q := 0; q < len(bucket[w]); q++ {nums[x] = bucket[w][q]x++}}//将位数增加dev *= 10mod *= 10}
}
func main() {rand.NewSource(time.Now().UnixNano())var nums1 []intfor i := 0; i < 10; i++ {nums1 = append(nums1, rand.Intn(100))}RadixSort(nums1)for i := 0; i < 10; i++ {fmt.Printf("%d ", nums1[i])}var nums2 []float64for i := 0; i < 10; i++ {nums2 = append(nums2, rand.Float64())}fmt.Println()BucketSort(nums2)for i := 0; i < len(nums2); i++ {fmt.Printf("%f ", nums2[i])}
}


http://www.mrgr.cn/news/91346.html

相关文章:

  • 垃圾回收器
  • java连接redis
  • javacv将视频切分为m3u8视频并播放
  • 【分布式理论11】分布式协同之分布式事务(一个应用操作多个资源):从刚性事务到柔性事务的演进
  • 基于Python豆瓣电影数据可视化分析系统的设计与实现
  • DFS算法篇:理解递归,熟悉递归,成为递归
  • 【NLP 25、模型训练方式】
  • 用 Python 实现 DeepSeek R1 本地化部署
  • 中药细粒度图像分类
  • Spring Cloud Gateway中断言路由和过滤器的使用
  • 深入解析 iOS 视频录制(一):录制管理核心MWRecordingController 类的设计与实现
  • C++编程,#include <iostream>详解,以及using namespace std;作用
  • compose multiplatform写一个简单的阅读器
  • Ubuntu 22.04.5 LTS 安装企业微信,(2025-02-17安装可行)
  • 最新Apache Hudi 1.0.1源码编译详细教程以及常见问题处理
  • 【PCIe 总线及设备入门学习专栏 1.1 -- PCI 设备访问方法】
  • 用deepseek学大模型08-卷积神经网络(CNN)
  • DeepSeek + Vue实战开发
  • ESP32 ESP-IDF TFT-LCD(ST7735 128x160) LVGL基本配置和使用
  • Blackbox.AI:高效智能的生产力工具新选择