排序算法 | 青训营笔记

95 阅读3分钟

1、冒泡排序

依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行,直到没有相邻元素需要交换,也就是说该元素列已经排序完成。

时间复杂度:O(n^2^)

算法稳定性:稳定

package main
import "fmt"

func Bubblesort(arr []int) []int {
	n := len(arr)
	for i := 0; i < n; i++ {
		for j := 0; j < n-i-1; j++ {
			if arr[j+1] < arr[j] {
				temp := arr[j+1]
				arr[j+1] = arr[j]
				arr[j] = temp
			}
		}
	}
	return arr
}

func main() {
	arr := []int{3, 4, 56, 1, 36, 5, 65, 0, -1, 90, 16, 33}
	arr = Bubblesort(arr)
	fmt.Println(arr)
}

2、选择排序

第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。

时间复杂度:O(n^2^)

算法稳定性:不稳定

package main
import "fmt"

func Selectsort(arr []int) []int {
	n := len(arr)
	for i := 0; i < n; i++ {
		index := i
		for j := i + 1; j < n; j++ {
			if arr[j] < arr[index] {
				index = j
			}
		}
		if index != i {
			arr[index], arr[i] = arr[i], arr[index]
		}
	}
	return arr
}

func main() {
	arr := []int{3, 4, 56, 1, 36, 5, 65, 0, -1, 90, 16, 33}
	arr = Selectsort(arr)
	fmt.Println(arr)
}

3、插入排序

插入排序是指在待排序的元素中,假设前面n-1个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序。

时间复杂度:在完全有序的情况下,插入排序每个未排序区间元素只需要比较1次,所以时间复杂度是O(n)。而在极端情况完全逆序,时间复杂度为O(n^2^)。

算法稳定性:稳定

package main
import "fmt"

func Insertsort(arr []int) []int {
	n := len(arr)
	for i := 0; i < n-1; i++ {
		if arr[i+1] < arr[i] {
			for j := i + 1; j > 0 && arr[j] < arr[j-1]; j-- {
				arr[j], arr[j-1] = arr[j-1], arr[j]
			}
		}
	}
	return arr
}

func main() {
	arr := []int{3, 4, 56, 1, 36, 5, 65, 0, -1, 90, 16, 33}
	arr = Insertsort(arr)
	fmt.Println(arr)
}

4、希尔排序

希尔排序是插入排序的一种又称“缩小增量排序”,把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。

时间复杂度:希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2^)好一些。希尔排序的时间复杂度为O(n^2/3^),希尔排序时间复杂度的下界是nlog2n。希尔排序没有快速排序算法快 O(nlog2n),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。

算法稳定性:不稳定

package main
import "fmt"

func Shellsort(arr []int) []int {
	n := len(arr)
	increment := n
	for true {
		increment = increment / 2
		for i := 0; i < increment; i++ {
			for j := i + increment; j < n; j += increment {
				for k := j; k > i && arr[k] < arr[k-increment]; k -= increment {
					arr[k], arr[k-increment] = arr[k-increment], arr[k]
				}
			}
		}
		if increment == 1 {
			break
		}
	}
	return arr
}

func main() {
	arr := []int{3, 4, 56, 1, 36, 5, 65, 0, -1, 90, 16, 33}
	arr = Shellsort(arr)
	fmt.Println(arr)
}

5、快速排序

(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。 (2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于分界值,而右边部分中各元素都大于或等于分界值。 (3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。 (4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

时间复杂度:理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为O(nlog2n)。 最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为O(n^2^)。 可以证明,快速排序的平均时间复杂度也是O(nlog2n)。

算法稳定性:不稳定

package main
import "fmt"

func Quacksort(arr []int, left, right int) {
	if left > right {
		return
	}
	i, j := left, right //设置哨兵i,j
	temp := arr[left]   // temp中存的是分界值
	for i != j {
		for arr[j] >= temp && i < j { //顺序很重要,要先从右往左找
			j--
		}
		for arr[i] <= temp && i < j { //再重左往右找
			i++
		}
		if i < j { // 但哨兵i和j没有相遇时
			arr[i], arr[j] = arr[j], arr[i] //交换两个数在数组中的位置
		}
	}
	//将分界值归位
	arr[left] = arr[i]
	arr[i] = temp
	Quacksort(arr, left, i-1)
	Quacksort(arr, i+1, right)
}

func main() {
	arr := []int{3, 4, 56, 1, 36, 5, 65, 0, -1, 90, 16, 33}
	Quacksort(arr, 0, len(arr)-1)
	fmt.Println(arr)
}

6、归并排序

该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

时间复杂度:O(nlog2n)

算法稳定性:稳定

package main
import "fmt"

func Mergesort(arr []int) []int {
	n := len(arr)
	if n <= 1 {
		return arr
	}
	left := Mergesort(arr[:n/2])
	right := Mergesort(arr[n/2:])
	return merge(left, right)
}

func merge(left, right []int) []int {
	l, r := 0, 0
	result := []int{}
	for l < len(left) && r < len(right) {
		if left[l] < right[r] {
			result = append(result, left[l])
			l++
		} else {
			result = append(result, right[r])
			r++
		}
	}
	result = append(result, left[l:]...)
	result = append(result, right[r:]...)
	return result
}

func main() {
	arr := []int{3, 4, 56, 1, 36, 5, 65, 0, -1, 90, 16, 33}
	slice := Mergesort(arr)
	fmt.Println(slice)
}

7、堆排序

1.首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端。 2.将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1。 3.将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组。 注意:升序用大根堆,降序就用小根堆(默认为升序)

时间复杂度:一般认为是O(nlog2n)级

算法稳定性:不稳定

package main

import "fmt"

func swap(a, b *int) {
	*a, *b = *b, *a
}

func Heapsort(nums []int) []int {
	// 堆排序,只能确认第一次个数是最大或最小的
	// 调换第一个元素和最后一个元素位置、从0倒数第二个继续堆排序
	i := len(nums)
	for i > 1 {
		buildHeep(nums, i)
		swap(&nums[0], &nums[i-1])
		i--
	}
	return nums
}

func buildHeep(nums []int, len int) {
	// 找到最后一个节点的父节点
	parent := len/2 - 1
	for parent >= 0 {
		heapify(nums, parent, len)
		parent--
	}
}

func heapify(nums []int, parent, len int) {
	// 判断两个子节点是否比父节点大,如果是的话替换
	max := parent
	lson := parent*2 + 1
	rson := parent*2 + 2
	if lson < len && nums[lson] > nums[max] {
		// 左节点是否大于父节点
		max = lson
	}
	if rson < len && nums[rson] > nums[max] {
		// 右节点是否大于父节点
		max = rson
	}
	if parent != max {
		swap(&nums[max], &nums[parent])
		heapify(nums, max, len)
	}
}

func main() {
	arr := []int{3, 4, 56, 1, 36, 5, 65, 0, -1, 90, 16, 33}
	Heapsort(arr)
	fmt.Println(arr)
}

8、桶排序

1、首先确定桶的个数。因为桶排序最好是将数据均匀地分散在各个桶中,那么桶的个数最好是应该根据数据的分散情况来确定。首先找出所有数据中的最大值mx和最小值mn; 根据max和min确定每个桶所装的数据的范围 size,有 size = (max - min) / n + 1,n为数据的个数,需要保证每个桶至少要能装1个数,故而需要加个1; 求得了size即知道了每个桶所装数据的范围,还需要计算出所需的桶的个数cnt,有 cnt = (max - min) / size + 1,需要保证至少有一个桶,故而需要加个1; 2、求得了size和cnt后,即可知第一个桶装的数据范围为 [mn, mn + size),第二个桶为 [mn + size, mn + 2 * size),…,以此类推 因此步骤2中需要再扫描一遍数组,将待排序的各个数放进对应的桶中。 3、对各个桶中的数据进行排序,可以使用其他的排序算法排序,例如快速排序;也可以递归使用桶排序进行排序; 4、将各个桶中排好序的数据依次输出,最后得到的数据即为最终有序。

时间复杂度:极限情况下每个桶只有一个数据时,桶排序的时间复杂度能够达到O(n)。极限情况下只有一个桶时,桶排序的时间复杂度取决于桶内的排序算法。

算法稳定性:桶排序是稳定的算法。【桶排序可以是稳定的。这取决于我们对每个桶中的元素采取何种排序方法,比如桶内元素的排序使用快速排序,那么桶排序就是不稳定的;如果使用的是插入排序,桶排序就是稳定的。】

package main

import (
	"fmt"
	"sort"
)

func Bucketsort(arr []int) []int {
	max := arr[0]
	min := arr[0]
	for i := 1; i < len(arr); i++ {
		if arr[i] > max {
			max = arr[i]
		}
		if arr[i] < min {
			min = arr[i]
		}
	}
	size := (max-min)/len(arr) + 1 // size 至少要为1
	cnt := (max-min)/size + 1      // 桶的个数至少要为1
	buckets := make([][]int, cnt)
	for i := 0; i < len(arr); i++ {
		idx := (arr[i] - min) / size
		buckets[idx] = append(buckets[idx], arr[i])
	}
	for i := 0; i < cnt; i++ {
		sort.Ints(buckets[i])
	}
	var result []int
	for i := 0; i < cnt; i++ {
		for j := 0; j < len(buckets[i]); j++ {
			result = append(result, buckets[i][j])
		}
	}
	return result
}

func main() {
	arr := []int{3, 4, 56, 1, 36, 5, 65, 0, -1, 90, 16, 33}
	result := Bucketsort(arr)
	fmt.Println(result)
}

排序算法的稳定性:通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。

做个笔记,如有错误欢迎大家指正!