堆
在许多算法中,需要大量用到如下两种操作:插入元素和寻找最大(小)值元素。为了提高这两种运算的效率,必须使用恰当的数据结构。
普通队列:易插入元素,但求最大(小)值元素需要搜索整个队列。
排序数组:易找到最大(小)值,但插入元素需要移动大量元素。
堆则是一种有效实现上述两种运算的简单数据结构。
定义:堆是一个几乎完全二叉树,每个节点都满足这样的特性:任一父节点的键值(key)不小于子节点的键值(最大堆)。
沿着每条从根到叶子的路径,元素键值以非升序排列。
可以类似地定义最小堆,这里以最大堆为准进行分析。
基本操作
MakeHeap
将一个数组改为堆,两种实现方式,第二种更快
1)从数组的第二个元素即下标为1的元素开始,不断地进行HeapInsert操作
2)从数组的最后一个元素开始,不断的进行heapify操作
func MakeHeap1(heap []int) {
for i := 1; i < len(heap); i++ {
HeapInsert(heap, i) //传下标
}
}
func MakeHeap2(heap []int) {
for i := len(heap) - 1; i >= 0; i-- {
heapify(heap, i, len(heap)) //传下标
}
}
HeapInsert(Push,ShiftUp)
1)将该元素与其父节点比较。
2)如果大于其父节点,则两者交换同时改变索引值,反之即该节点找到其合适位置。
3)重复上述操作
func HeapInsert(heap []int, index int) {
for index != 0 {
if heap[index] > heap[(index-1)/2] {
heap[index], heap[(index-1)/2] = heap[(index-1)/2], heap[index]
index = (index - 1) / 2
} else {
return
}
}
}
//该函数适宜makeheap,但是中途插入一个值只传索引不传参数会有些奇怪。但是可以再传一个value
//的参数,在for之前改变heap[index] = value,随后进行上述操作可以实现在不破坏堆结构的情况
//下修改某个位置的值。
func HeapInsert(heap *[]int, value int) {//该版本更适合进行中途插入
index := len(*heap)
*heap = append(*heap, value)
for index != 0 {
if (*heap)[index] > (*heap)[(index-1)/2] {
(*heap)[index], (*heap)[(index-1)/2] = (*heap)[(index-1)/2], (*heap)[index]
index = (index - 1) / 2
} else {
return
}
}
}
heapify(ShiftDown)
1)首先找到该元素的左右子节点中的更大者,若无子节点则说明已经到达合适位置。
2)随后将该节点与之比较,小于则交换,同时改变索引值。反之找到合适位置。
3)重复上述过程,直到到达合适位置
func Heapify(heap []int, index, heapsize int) { //往下走
var largest int
for index*2+1 < heapsize {//有左子节点
largest = 2*index + 1 //先让最小值为左子节点的值
if (2*index + 2) < heapsize { //再判断有没有右子节点
if heap[2*index+2] > heap[2*index+1] { //右子节点更大
largest = 2*index + 2
}
}
if heap[largest] > heap[index] { //孩子比父亲大
heap[index], heap[largest] = heap[largest], heap[index]
index = largest
} else {
break
}
}
}
delete(Pop)
1)传过来的是堆的大小,--后得到最后一个元素的索引
2)将要删除的元素和最后一个元素互换
3)再进行Heapify,此时传参heapsize已经减小了1。
4)最后对切片进行切片,使得切片长度减1。
func Delete(heap *[]int, index, heapsize int) { //heapsize的最右边的大小
heapsize--
(*heap)[index], (*heap)[heapsize] = (*heap)[heapsize], (*heap)[index]
Heapify((*heap), index, heapsize)
*heap = (*heap)[:heapsize]
}
HeapSort
!堆才能heapsort进行排序,也可以把makeheap整合进来,但是我觉得没有必要就没做。
1)首先找到堆的右边界下标
2)再判断当右边界>0时,将0处的最大值和堆的最后一个元素互换,从而得到最大值。
3)对右边界大小减一再进行heapify操作,这里传入rightboundry则在进行heapify时不会误操作到最后已得到的元素。
4)重复上述操作
func HeapSort(heap []int) { //先用makeheap变成堆结构再排序
rightboundry:= len(heap) - 1
for rightboundry > 0 {
heap[0], heap[rightboundry] = heap[rightboundry], heap[0]
rightboundry--
heapify(heap, 0, rightboundry)
}
}
题目练习
已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
题解:首先由题目可知一个元素离它有序时所应在的位置的大小不超过k,反言之即一个位置离数组有序时的元素的位置不超过k。所以对于0号位置,我们只需要找后面的六个元素中最小的即可。这里用插入排序等均可,但是今天学习的堆,就选择使用最小堆来解决这个问题。
func MakeMinHeap(arr []int, k int) {
MinHeap := &Heap{
heap: make([]int, 0),
}
index := 0
for index < min(len(arr), k) {
MinHeap.MinHeapInsert(arr[index])
index++
}
fmt.Println(MinHeap.heap)
i := 0
for index < len(arr) {
arr[i] = MinHeap.MinHeapPop()
MinHeap.MinHeapInsert(arr[index])
i++
index++
}
for i != index {
arr[i] = MinHeap.MinHeapPop()
i++
}
}
func (this *Heap) MinHeapInsert(value int) {
index := len(this.heap)
this.heap = append(this.heap, value)
for index != 0 {
if this.heap[index] < this.heap[(index-1)/2] {
this.heap[index], this.heap[(index-1)/2] = this.heap[(index-1)/2], this.heap[index]
index = (index - 1) / 2
} else {
return
}
}
}
func (this *Heap) MinHeapPop() int {
heapsize := len(this.heap) - 1
this.heap[0], this.heap[heapsize] = this.heap[heapsize], this.heap[0]
res := this.heap[heapsize]
this.MinHeapify(0, heapsize)
this.heap = this.heap[:heapsize]
return res
}
func (this *Heap) MinHeapify(index int, heapsize int) { //往下走
var min int
for index*2+1 < heapsize { //有左子节点
min = 2*index + 1 //让最小值为左子节点
if (2*index + 2) < heapsize { //还有右子节点
if this.heap[2*index+2] < this.heap[2*index+1] { //判断左右子节点何者更小
min = 2*index + 2
}
}
if this.heap[min] < this.heap[index] { //子节点比父节点小
this.heap[index], this.heap[min] = this.heap[min], this.heap[index]
index = min
} else {
break
}
}
}
相当于使用一个大小为k的堆来不断的往里面插入待排序数组中元素,同时pop出最小堆中的根节点。如果k==len(待排序数组),则相当于普通堆排序。
比较器:重载运算符
golang中两种重载运算符方式,其一通过函数,其二通过接口。
计数排序
非基于比较排序
func CountingSort(arr []int, min, max int) {
size := max - min + 1
count := make([]int, size) //此时下标0代表min
for i := 0; i < len(arr); i++ {
count[arr[i]-min]++
}
var i int = 0
for j := 0; j < size; j++ {
for count[j] != 0 {
arr[i] = j + min
count[j]--
i++
}
}
}
基数排序
朴素实现方式
大致类似于计数排序,但是这里用十个队列来实现给每个位置进行排序。
func (this *Queue) IsEmpty() bool {
if this.rear == this.front {
return true
} else {
return false
}
}
func (this *Queue) Push(value int) {
this.queue = append(this.queue, value)
this.rear++
}
func (this *Queue) Pop() (res int) {
if this.front == this.rear {
fmt.Println("Pop err:队列为空")
return -1
}
res = this.queue[this.front]
if len(this.queue) > 1 {
for i := 0; i < this.rear-1; i++ {
this.queue[i] = this.queue[i+1]
}
this.rear--
this.queue = this.queue[0:this.rear]
return res
} else {
this.rear--
this.queue = nil
return res
}
}
func RadixSort(arr []int) {
max := Max(arr)
digit := 0
for max != 0 {
digit++
max = max / 10
}
radis := make([]Queue, 10)
for time := 1; time <= digit; time++ {
a := int(math.Pow(10.0, float64(time))) //求余
b := int(math.Pow(10.0, float64(time-1))) //除
for i := 0; i < len(arr); i++ {
r := (arr[i] % a / b)
radis[r].Push(arr[i])
}
i := 0
for j := 0; j < 10; j++ {
for !radis[j].IsEmpty() {
arr[i] = radis[j].Pop()
i++
}
}
}
}
func Max(arr []int) int {
res := arr[0]
for i := 1; i < len(arr); i++ {
if arr[i] > res {
res = arr[i]
}
}
return res
}
桶排序实现方式
从前往后遍历方式
func BuckerSort(arr []int) {
max := Max(arr)
digit := 0
for max != 0 {
digit++
max = max / 10
}
bucket := make([]int, len(arr))
for time := 1; time <= digit; time++ {
count := make([]int, 10)
a := int(math.Pow(10.0, float64(time))) //求余
b := int(math.Pow(10.0, float64(time-1))) //除
for i := 0; i < len(arr); i++ { //第一次进行计数排序,求出各个数的词频
r := (arr[i] % a / b)
count[r]++
}
for i := 1; i < 10; i++ { //第二个对计数数组进行前缀和运算,是为了找到本轮所求位上数为该值的数的位置分布
count[i] = count[i] + count[i-1]
}
for i := len(arr) - 1; i >= 0; i-- {
r := (arr[i] % a / b)
bucket[count[r]-1] = arr[i]
count[r]--
}
//这个是最难以理解的,当我们有了上面的前缀和数组后,假设count如下
//2 3 3 4 5……
//0 1 2 3 4……
//则我们可知该位上为0的数有两个,为1的有一个,为2的没有,为3的有一个,为4的也有一个
//随后我们从后往前遍历,首先找到最后一个数,假设在本轮中求的是个位,那么所求的是0
//那么它所要放到的位置就是2-1。那么为什么是2-1而不是放到第0位置呢。因为这里是第一轮不好想象。
//但如果是第二轮这个时候有两个十位均为5的数,他们谁大谁小呢?显然是后面的大,因为第一轮排序中二者的个位已经从小到大排好了。
//所以在第一轮中则这里的谁先谁后就是无所谓的
//而进一步会发现所以这里该位为0的数所占据的位置会是0和1,该位为1的数占据的是2,为2的不存在,故而为3的占据3,从而实现有序
//如果还不懂可以私信我
for i := 0; i < len(arr); i++ {
arr[i] = bucket[i]
}
}
}
func Max(arr []int) int {
res := arr[0]
for i := 1; i < len(arr); i++ {
if arr[i] > res {
res = arr[i]
}
}
return res
}
从后往前遍历方式
/*为了下面行文方便,假设本轮为十位的轮次
在最关键的代码中这里是实现的从后往前遍历,那么可不可以从前往后呢?显然是可以的。
从后往前遍历的好处是我知道我找到的数就是十位相同下个位最大的数,反之从前往后我可以知道这个相同十位下个位是最小的数,同时可以通过前缀和来标记下一个相同十位的数应该放到的位置。
2 3 5 5 7……
0 1 2 3 4……
在这个例子中,假如我们从后往前遍历到的第一个数是十位为4的数,同时通过上一轮的排序我们知道它的个位也是最大的,那么他的位置显然就是7-1=6,
然后count[ r ]--,
2 3 5 5 6……
0 1 2 3 4……
这样下一个十位为4的就是6-1。
换言之这里的前缀和起到了记录索引的功能
而从前往后假如遍历到的第一个数是十位为4的数,同时通过上一轮的排序我们知道它的个位也是最小的,那么他的位置就是前面的0123要使用的5个位置,所以这个数的在数组上的位置是5,然后也进行count[ r ]--,
2 3 5 5 6……
0 1 2 3 4……
那么问题就在这里,下一个十位为4的数的位置是什么呢,通过前缀和我们只能知道,前面要使用五个位置,同时我这个位置还有一个数,我们根本不知道前面是否已经有过十位上相同的数进入到了bucket中。那么这样我们就需要一个数组来存储每个数要储存的位置。那么count能存储前缀和,为什么不能存储位置呢,而这个位置也不过是前缀和数组右移一个数罢了。
2 1 2 0 1……
0 1 2 3 4……//计数
0 2 3 5 5……
0 1 2 3 4……//每个数的首要元素存储的地方,即前缀和右移
然后假如方式一个41,十位为4,则要count[4]++。
所以我们会发现从前往后也可以,而多的开销是9次循环。
func BuckerSort(arr []int) {
max := Max(arr)
digit := 0
for max != 0 {
digit++
max = max / 10
}
bucket := make([]int, len(arr))
for time := 1; time <= digit; time++ {
count := make([]int, 10)
a := int(math.Pow(10.0, float64(time))) //求余
b := int(math.Pow(10.0, float64(time-1))) //除
for i := 0; i < len(arr); i++ { //第一次进行计数排序,求出各个数的词频
r := (arr[i] % a / b)
count[r]++
}
for i := 1; i < 10; i++ { //第二个对计数数组进行前缀和运算,是为了找到本轮所求位上数为该值的数的位置分布
count[i] = count[i] + count[i-1]
}
for i := 9; i >= 1; i-- { //记录下一个数要放的位置
count[i] = count[i-1]
}
count[0] = 0
for i := 0; i < len(arr); i++ {
r := (arr[i] % a / b)
bucket[count[r]] = arr[i]
count[r]++
}
for i := 0; i < len(arr); i++ {
arr[i] = bucket[i]
}
}
}
func Max(arr []int) int {
res := arr[0]
for i := 1; i < len(arr); i++ {
if arr[i] > res {
res = arr[i]
}
}
return res
}