算法2---归并排序与快速排序(golang)

100 阅读4分钟
  • 取中值 mid:=(left+right)/2 //有可能越界 mid:=left+(right-left)/2 //改进1 mid := left + (right-left)>>1 //改进2 右移一位比除以2快

  • 用递归来找数组最大值

    解题思路:

    二分法取左边的最大值与右边的最大值比较 左边的最大值如何得到呢 将左边的最大值二分法取左右两边的最大值比较 以此递归下去

    graph TB
    
    A((1 0 3 5 9 12))
    B((1 0 3 ))
    C((5 9 12))
    D((1 0))
    E((3))
    F((5 9))
    G((12))
    H((1))
    I((0))
    J((5))
    K((9))
    
    A-->B
    A-->C
    B-->D
    B-->E
    C-->F
    C-->G
    D-->H
    D-->I
    F-->J
    F-->K
    
    func Getmax(arr []int, left, right int) int {
    	if left == right {
    		return arr[left]
    	}
    	mid := left + (right-left)>>1
    	leftMax := Getmax(arr, left, mid)
    	rightMax := Getmax(arr, mid+1, right)
    	return max(leftMax, rightMax)
    }
    
    func max(a, b int) int {
    	if a < b {
    		a = b
    	}
    	return a
    }
    
  • 归并排序 将数组 一直二分到单个数,而单个数必然是有序的可直接返回。 难点在于merge方法,该方法将左边的有序集合 与 右边的有序集合进行归并。

    如0 1 9 和3 5 12 ,先用双指针分别指向最左侧的0 和 3 ,谁小就塞进help数组,同时该指针往右移动一位,该指针越界,则将另一侧的数据填进help。

    问题1:为什么左边和右边是有序的集合? 答:因为从单个数开始保证是有序的,而递归merge成两个数的时候也进行了排序 0 3

    问题2:为什么最左侧的0小于3就可以塞进help数据,它不用跟右侧其他数进行比较? 答:因为右侧也是有序的,3>0,而3右边的数都是>3,就必定>0 (这里也是比选择排序\冒泡排序\插入排序(O(n^2))等优势的地方,减少了很多重复的比较,充分利用子资源,符合master公式,时间复杂度做到了O(n*logn))

    
    // 归并排序
    func process(arr []int, left, right int) {
    	if left == right {
    		return
    	}
    	mid := left + (right-left)>>1
    	process(arr, left, mid)
    	process(arr, mid+1, right)
    	merge(arr, left, mid, right)
    }
    
    func merge(arr []int, left, mid, right int) {
    	hLenght := right - left + 1
    	help := make([]int, hLenght)
    	i := 0
    	p1 := left
    	p2 := mid + 1
    	for p1 <= mid && p2 <= right {
    		if arr[p1] <= arr[p2] {
    			help[i] = arr[p1]
    			i++
    			p1++
    		} else {
    			help[i] = arr[p2]
    			i++
    			p2++
    		}
    	}
    	for p1 <= mid {
    		help[i] = arr[p1]
    		i++
    		p1++
    	}
    	for p2 <= right {
    		help[i] = arr[p2]
    		i++
    		p2++
    	}
    	for i = 0; i < hLenght; i++ {
    		arr[left+i] = help[i]
    	}
    }
    
    func main() {
    	arr := []int{1, 0, 9, 5, 12, 3}
    	process(arr, 0, 5)
    	fmt.Println(arr)
    	}
    
  • 排序优劣 选择排序,第一次需要遍历n个数,才能找到arr[0] 第二次需要遍历n-1个数,才能找到arr[1] 第三次需要遍历n-2个数,才能找到arr[2] 并且第一次,第二次,第三次之间的运算都是独立的,浪费了很多比较行为,而归并排序的比较信息是可以往下传递的,所以能做到O(n*logn)

  • 题目:求{1, 3, 4, 2, 5}的小和

在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和

举例4,左侧比它小的数有1 3 ,4的小和就是4 2,左侧比他小的数有1,2的小和就是1 ...

解题思路: 求所有的小和,也可以转换为右边比1大的数的个数1+右边比3大的个数3+右边比4大的个数4+右边比2大的个数2,所以这里我们改版上面的归并算法


// 归并排序
func process(arr []int, left, right int) int {
	if left == right {
		return 0
	}
	mid := left + (right-left)>>1
	return process(arr, left, mid) +
		process(arr, mid+1, right) +
		merge(arr, left, mid, right)
}

func merge(arr []int, left, mid, right int) int {
	hLenght := right - left + 1
	help := make([]int, hLenght)
	i := 0
	p1 := left
	p2 := mid + 1

	smallSum := 0
	for p1 <= mid && p2 <= right {
		fmt.Println(p1, arr[p1], p2, arr[p2])
		if arr[p1] < arr[p2] {
			//只有左侧比右侧小的情况才产生小和
			smallSum += (right - p2 + 1) * arr[p1] //当1 < 3 时,产生3 5 9(right-p2+1)个1的小和
			help[i] = arr[p1]
			i++
			p1++
		} else {
			help[i] = arr[p2]
			i++
			p2++
		}
	}
	for p1 <= mid {
		help[i] = arr[p1]
		i++
		p1++
	}
	for p2 <= right {
		help[i] = arr[p2]
		i++
		p2++
	}
	for i = 0; i < hLenght; i++ {
		arr[left+i] = help[i]
	}
	return smallSum
}
func main() {
	arr := []int{1, 3, 4, 2, 5}
	fmt.Println(process(arr, 0, len(arr)-1))
}
  • 快速排序 思路: 问题:给定一个乱序数组1,3,4,2,5,3,要求比最右边的3小的数放在左边,等于3的数放在中间,大于3的数放在右边。

    答: 1.指定左边界下标p1为-1,右边界下标p2为5 2.接着从i=0遍历数组,若arr[i]小于最右侧数,将p1右移,交换p1和i位置的数,i右移。若arr[i]等于最右侧数,i右移。若arr[i]大于最右侧数,将p2左移,交换p2和i位置的数,i不移动。 3.第1次得到1,|3,4,2,5,|3 第2次得到1,|3,5,2,|4,3 第3次得到1,|3,2,|5,4,3 第4次得到1,2,3,||5,4,3 p1=p2 退出循环 将p2与最右侧3交换得到 1,2,3,3,4,5

快排3.0就是将原数组分为三份,左边是小于等于a的,中间是等于a的,右边是大于a的,虽然左边和右边依然乱序,但是a的位置是顺序的!!!,我们就定位到了a的位置。然后我们将左边和右边的数组分别分为三份,又确定了左a和右a的位置,一直切割下去直到只有一个数(单数就是有序)

我们如何选取a呢,如果选择最右侧的,存在最极端的情况是1,2,3,4,5,只切割成T(4/5)+T(1/5),接近O(n^2),而取到3,能很好的均分,所以我们a选择其中的随机数,就能做到O(n*logn)

代码实现:


func quickSort(arr []int) {
	if arr == nil || len(arr) < 2 {
		return
	}
	qS(arr, 0, len(arr)-1)
}

func qS(arr []int, l, r int) {

	if l < r {
		swap(arr, l+rand.Intn(r-l+1), r) //将最右边的数与随机数交换
		p := partition(arr, l, r)
		qS(arr, l, p[0]-1)
		qS(arr, p[1]+1, r)
	}
}

// 该方法就是取最右边的数为锚点
// 小于最右边的数放在左侧 等于放中间 大于放右边
// 返回中间数的最左侧 和最右侧下标
func partition(arr []int, l, r int) []int {
	p1 := l - 1  //左侧小于目标数下标
	p2 := r      //右侧大于目标数结界 这里没有加一是因为把最右边的数当做比较值
	for l < p2 { //当遍历到大于结界就停止 不需要遍历到最右侧
		if arr[l] < arr[r] {
			p1++
			swap(arr, p1, l)
			l++
		} else if arr[l] == arr[r] {
			l++
		} else {
			p2--
			swap(arr, l, p2)
		}
	}
	swap(arr, p2, r)         //将最右侧的3 放到右侧结界处
	return []int{p1 + 1, p2} //返回中间数据的最左边和最右边
}

func main() {
	arr := []int{1, 3, 4, 2, 5, 3}
	quickSort(arr)
	fmt.Println(arr)
}