Go语言实现排序算法

最近在学习算法中,看的视频是Liuyubobobo视频课,感觉看了视频之后还是得自己敲一下代码才行。部分笔记,图片都是源于视频课,部分笔记与代码源于leetcode cookbook。

O ( n 2 ) O(n^2) O(n2)的算法在某些情况下会更适用

1.选择排序

O ( n 2 ) O(n^2) O(n2)

for i=0:n{

寻找[i,n)区间里的最小值

}

2.插入排序

O ( n 2 ) O(n^2) O(n2)

类似于打扑克牌时的排序,跟左边的所有牌挨个比较

改进:在遍历之后不是马上进行插入,将当前元素copy一个副本,找到其合适的插入位置,而不是一直与前一个元素进行交换操作(调用swap函数)。——在近乎有序的情况下性能很好。

对小数组进行插入排序

3.冒泡排序

双循环,每轮确定一个数的位置

4.希尔排序

5.归并排序

Mergesort

自顶向下

Merge-sort()

  • 分解:将序列分成 ⌊ n / 2 ⌋ \lfloor n/2 \rfloor n/2长度的2个子序列 Merge-sort()
  • 解决:用归并排序递归地排序子序列 Merge-sort()
  • 合并:合并两个已排序的子序列得到答案 Merge()

Merge函数(考虑数组越界):加上两个哨兵牌∞ / 加左右两个哨兵left与right

改进:

  1. 在merge中,如果左边序列已经大于右边(即左边最后一位与右边第一位):加入判断
  2. 如果不断分割序列后的长度到定值时,用插入排序进行排序。

自底向上

双循环 0~sz-1, 1~2sz-1, 2~3sz-1

sz = 1 :merge([0] [1]) ; merge([2] [3] ) #先是每两个元素进行排序

sz = 2 :merge([0,1] [2,3 ])

sz = 4 :merge([0,1,2,3])
在这里插入图片描述
考虑到数组出界的问题:
在这里插入图片描述

代码

// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
func merge(arr []int, l int, mid int, r int) {
	// 经测试,传递aux数组的性能效果并不好
	copy := make([]int, r+1)
	for i := l; i <= r; i++ {
		copy[i] = arr[i]
	}
	i, j := l, mid+1
	// 判断边界条件
	for k := l; k <= r; k++ {
		if i > mid {
			arr[k] = copy[j]
			j++
		} else if j > r {
			arr[k] = copy[i]
			i++
		} else if copy[i] < copy[j] {
			arr[k] = copy[i]
			i++
		} else {
			arr[k] = copy[j]
			j++
		}

	}
}

func mergesort(arr []int, l int, r int) {
	if l >= r {
		return
	}
	mid := (l + r) / 2
	mergesort(arr, l, mid)
	mergesort(arr, mid+1, r)
	merge(arr, l, mid, r)
}

6.快速排序

快速排序是在原地排序,不需要合并操作,这点可以与归并排序进行比较一下

Quicksort

分治思想

关键是Partition过程,主元可以选择首个元素或末尾元素

分成两部分<v , >=v
在这里插入图片描述

[tip: 一定长度的子数组,用插入排序]

改进:随机快速排序 Quicksort1

Quicksort 对近乎有序的数组快速排序性能很差——(选择首个元素时)只有右子树,退化为 O ( n 2 ) O(n^2) O(n2)
**改进:**随机选择一个元素作为主元

改进:双路快速排序 Quicksort2

当=v的元素较多时,序列会被分成很不平衡的两部分——退化为 O ( n 2 ) O(n^2) O(n2)
改进:两个扫描方向,分别从两端往中间,直到满足交换条件(左边指针i的值>v,右边<v)
在这里插入图片描述

改进:三路快速排序 Quicksort3

三路快速排序法,增加了一路 ==v
在这里插入图片描述

代码

// 参数传递应该是地址传递?
// slice本身就是引用类型,相当于引用传递了

//快速排序,一定长度的子数组,用插入排序
func partition(nums []int, l int, r int) int {
	v := nums[l]
	// <=v: nums[l+1,j], >v: nums[j+1,i-1]
	j := l
	for i := l + 1; i <= r; i++ {
		if nums[i] < v {
			j++
			nums[i], nums[j] = nums[j], nums[i]
		}
	}
	nums[j], nums[l] = nums[l], nums[j]
	return j
}

func quicksort(nums []int, l int, r int) {
	if l >= r {
		return
	}
	// nums[l,p-1],nums[p+1,r]
	p := partition(nums, l, r)
	quicksort(nums, l, p-1)
	quicksort(nums, p+1, r) //为什么是p+1?,p位置元素就是主元
}
-----------------------------------------------------------
func partition1(nums []int, l int, r int) int {
	//不初始化Seed的话,生成的是伪随机数,每次运行程序都是一样的(主函数中加入初始化)
	n := rand.Intn(len(nums))
	fmt.Println(n)
	nums[l], nums[n] = nums[n], nums[l]
	v := nums[l]
	// <=v: nums[l+1,j], >v: nums[j+1,i-1]
	j := l
	for i := l + 1; i <= r; i++ {
		if nums[i] < v {
			j++
			nums[i], nums[j] = nums[j], nums[i]
		}
	}
	nums[j], nums[l] = nums[l], nums[j]
	return j
}

func quicksort1(nums []int, l int, r int) {
	if l >= r {
		return
	}
	// nums[l,p-1],nums[p+1,r]
	p := partition1(nums, l, r)
	quicksort1(nums, l, p-1)
	quicksort1(nums, p+1, r) //为什么是p+1?,p位置元素就是主元
}
func main() {
	// 我们一般使用系统时间的不确定性来进行初始化
	rand.Seed(time.Now().Unix())
	nums := []int{1, 6, 5, 8, 6, 8, 9, 4, 4, 5, 10}
	quicksort1(nums, 0, len(nums)-1)
	fmt.Println(nums)
}

-----------------------------------------------------------
// 改进2:从两端开始扫描
func partition2(nums []int, l int, r int) int {
	v := nums[l]
	i, j := l+1, len(nums)-1
	// nums[l+1,i-1] <v , nums[j,r] >v
	for true {
		for i <= r && nums[i] < v {
			i++
		}
		for j >= l+1 && nums[j] > v {
			j--
		}
		if i > j {
			break
		}
		nums[i], nums[j] = nums[j], nums[i]
		i++
		j--
	}
	nums[l], nums[j] = nums[j], nums[l]
	return j
}

func quicksort2(nums []int, l int, r int) {
	if l >= r {
		return
	}
	// i从左边开始扫描,j从右边
	p := partition2(nums, l, r)
	quicksort2(nums, l, p-1)
	quicksort2(nums, p+1, r)
}
-----------------------------------------------------------
//改进3:三路快排法
func partition3(nums []int, l int, r int) (int, int) {
	v := nums[l]
	lt, gt := l, r+1
	i := l + 1
	// nums[l+1,lt] <v , nums[lt+1,i)==v, nums[gt,r] >v
	for i < gt {
		if nums[i] < v {
			nums[i], nums[lt+1] = nums[lt+1], nums[i]
			i++
			lt++
		} else if nums[i] > v {
			nums[i], nums[gt-1] = nums[gt-1], nums[i]
			// i++
			//gt-1未知,所以i不往后挪
			gt--
		} else {
			i++
		}
	}
	nums[l], nums[lt] = nums[lt], nums[l]
	return lt, gt
}

func quicksort3(nums []int, l int, r int) {
	if l >= r {
		return
	}
	// i从左边开始扫描,j从右边
	p1, p2 := partition3(nums, l, r)
	quicksort3(nums, l, p1-1)
	quicksort3(nums, p2, r)

}

归并与快速排序衍生问题
分治算法:

堆排序

堆的实际应用不是排序,常用于实现优先队列。

优先队列的实现:普通数组 / 顺序数组 / 堆
在这里插入图片描述

二叉堆的性质:

  1. 是完全二叉树:父节点,左节点,右节点的下标有一定关系
  2. 大顶堆 / 小顶堆

用数组存储二叉堆(0处是否设为空):
在这里插入图片描述

Heap sort

堆的基本操作:添加元素 / 取元素

  • 向堆添加元素(siftup函数):(不断上浮操作:与父节点比较大小)
  • 从堆中取元素,调整堆(siftdown函数): 堆顶和堆的末尾元素进行交换,交换后删除尾节点,然后再重新对堆进行调整,(不断下沉操作,与子节点进行比较)

堆上的2种操作:heapify / replace

  • replace:取出最大元素后,放入一个新元素

实现1:先extractMax 再add,两次 O ( l o g n ) O(logn) O(logn)

**实现2:**直接将堆顶元素替换为新元素,再用siftdown,一次 O ( l o g n ) O(logn) O(logn)

  • heapify:把数组整理成堆的样子

如何找到最后一个非叶子节点:先找到最后一个叶子节点(n-1),再找其父节点

** 实现:**从倒数第一个非叶子节点往上siftup操作,再往下siftdown操作,算法复杂度是 O ( n ) O(n) O(n)

Init实现:如果是将n个元素逐个插入到一个空堆中,算法复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)

改进1:原地堆排序

在原地将数组整理成有序数组,而不用一直pop操作来获得有序操作。
思路:
如何在原地进行排序:把原始数组直接看成一个堆

  1. 通过heapify操作,将数组构建成最大堆(此时,数组首元素v为最大值,应该去数组的末端)请添加图片描述

  2. 将当前最大值v与末尾元素w进行调换请添加图片描述

  3. 调换后,此时橙色部分不为最大堆,需要将w进行siftdown操作,从而使当前部分成为最大堆。在这里插入图片描述

  4. 重复以上操作,将当前最大堆的首元素与堆的末尾元素进行调换,v去到了数组倒数第二位。在这里插入图片描述

  5. repeat…

代码

数组排序的拓展应用

归并思想求逆序对

归并过程中进行计数
在这里插入图片描述

代码:

// 归并算法的思想来求逆序对
func revers(arr []int, l int, r int) {
	if l >= r {
		return
	}
	mid := (l + r) / 2
	revers(arr, l, mid)
	revers(arr, mid+1, r)
	merge(arr, l, mid, r) //归并过程中进行计数
}

// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
func merge(arr []int, l int, mid int, r int) {
	// 经测试,传递aux数组的性能效果并不好
	copy := make([]int, r+1)
	for i := l; i <= r; i++ {
		copy[i] = arr[i]
	}
	i, j := l, mid+1
	// 判断边界条件
	for k := l; k <= r; k++ {
		if i > mid {
			arr[k] = copy[j]
			j++
		} else if j > r {
			arr[k] = copy[i]
			i++
		} else if copy[i] < copy[j] {
			arr[k] = copy[i]
			i++
		} else {
			arr[k] = copy[j]
			j++
			count += (mid - i + 1)
		}
		// 注意!
		// 此时, 因为右半部分j所指的元素小
		// 这个元素和左半部分的所有未处理的元素都构成了逆序数对
		// 左半部分此时未处理的元素个数为 mid - j + 1

	}
}

Top K问题

快排解决Top K问题

思路:首先要清楚快速排序中的partition操作
一次partition操作后,通过主元v的位置下标m可以得到:
1. m=k:取前m个元素
2. m<k:对右侧数组递归地进行partition,找前n-k个元素
3. m>k:对左侧数组递归地进行partiion,找前k个元素

堆排序解决Top K 问题

如果要求取的是最小的k个数,则用最大堆来维护这k个数(pop出最大的前n-k个数后,留下的就是答案)。

Top K问题的方法对比

Top K问题有两种不同的解法,一种解法使用堆(优先队列),另一种解法使用类似快速排序的分治法。
在面试中,另一个常常问的问题就是这两种方法有何优劣。看起来分治法的快速选择算法的时间、空间复杂度都优于使用堆的方法,但是要注意到快速选择算法的几点局限性:

第一,算法需要修改原数组,如果原数组不能修改的话,还需要拷贝一份数组,空间复杂度就上去了。

第二,算法需要保存所有的数据。如果把数据看成输入流的话,使用堆的方法是来一个处理一个,不需要保存数据,只需要保存 k 个元素的最大堆。而快速选择的方法需要先保存下来所有的数据,再运行算法。当数据量非常大的时候,甚至内存都放不下的时候,就麻烦了。所以当数据量大的时候还是用基于堆的方法比较好。

代码

剑指offer40题
解法一(最大堆):

type Intheap []int

func (h Intheap) Len() int {
	return len(h)
}
func (h Intheap) Less(i, j int) bool {
	return h[i] > h[j]
}
func (h Intheap) Swap(i, j int) {
	h[i], h[j] = h[j], h[i]
}
func (h *Intheap) Push(x interface{}) {
	*h = append(*h, x.(int))
}
func (h *Intheap) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[:n-1]
	return x
}

func getLeastNumbers(arr []int, k int) []int {
h := &Intheap{}
	for _, v := range arr {
		heap.Push(h, v)
	}
	for i := 0; i < len(arr)-k; i++ {
		heap.Pop(h)
	}
	return *h
}

解法二(快排思想):

func getLeastNumbers(arr []int, k int) []int {
    if k==0{
        return nil
    }
	partionarr(arr, 0, len(arr)-1, k)
	return arr[0 : k]
}

//递归思想
func partionarr(arr []int, l, r int, k int) {
	p := partition(arr, l, r)
	if p == k-1 {  //需要考虑k=0时
		return
	} else if p > k-1 {
		partionarr(arr, l, p-1, k)
	} else {
		partionarr(arr, p+1, r, k)
	}
}

//  快排忘记了
//[l+1,j] <=v,   [j+1,r]>v

func partition(arr []int, l, r int) int {
	v := arr[l]
	j := l
	for i := l + 1; i <= r; i++ {
		if arr[i] < v {
			arr[i], arr[j+1] = arr[j+1], arr[i]
			j++
		}
	}
	arr[j], arr[l] = arr[l], arr[j]
	return j
}

参考:https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/solution/tu-jie-top-k-wen-ti-de-liang-chong-jie-fa-you-lie-/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值