-
取中值
mid:=(left+right)/2 //有可能越界
mid:=left+(right-left)/2 //改进1
mid := left + (right-left)>>1 //改进2 右移一位比除以2快 -
用递归来找数组最大值
解题思路:
二分法取左边的最大值与右边的最大值比较
左边的最大值如何得到呢
将左边的最大值二分法取左右两边的最大值比较
以此递归下去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)
}