排序算法(1):堆排序

图解堆排序

摘要:
 堆排序是一种树形选择排序,在排序过程中可以把元素看成是一颗完全二叉树,每个节点都大(小)于它的两个子节点,当每个节点都大于等于它的两个子节点时,就称为大顶堆,也叫堆有序; 当每个节点都小于等于它的两个子节点时,就称为小顶堆。


大顶堆.png

小顶堆.png

算法思想(以大顶堆为例):
1.将长度为n的待排序的数组进行堆有序化构造成一个大顶堆

2.将根节点与尾节点交换并输出此时的尾节点

3.将剩余的n -1个节点重新进行堆有序化

4.重复步骤2,步骤3直至构造成一个有序序列

代码实现:
package heapsort

type Node struct {
    data  int
    left  *Node
    right *Node
}

type Tree struct {
    root *Node
}

func InitHeap(array []int) *Tree {
    tree := new(Tree)
    nodeArray := make([]*Node, 0)
    for _, a := range array {
        nodeArray = append(nodeArray, &Node{
            data:  a,
            left:  nil,
            right: nil,
        })
    }

    for i := 0; i <= len(nodeArray)/2-1; i++ {
        j := i*2 + 1
        if i == 0 {
            tree.root = nodeArray[i]
        }
        for {
            temp := nodeArray[i].data
            if j+1 > len(nodeArray) {
                break
            }
            z := j
            if nodeArray[j+1].data >= nodeArray[j].data {
                z = j + 1
            }
            if temp > nodeArray[z].data {
                break
            } else {
                nodeArray[i].data = nodeArray[z].data
                nodeArray[z].data = temp
            }
            nodeArray[i].left = nodeArray[j]
            nodeArray[i].right = nodeArray[j+1]
            i = j
            j = i*2 + 1
        }

    }
    return tree
}

func AdJustDown(array []int, len int) {
    for i := len/2 - 1; i >= 0; i-- {
        j := i*2 + 1
        temp := array[i]
        if j > (len - 1) {
            break
        }
        if j+1 < (len-1) && array[j] < array[j+1] {
            j++
        }
        if temp < array[j] {
            array[i] = array[j]
            array[j] = temp
        }
    }
}

func HeapSort(array []int) (int, []int) {
    max := array[0]
    array[0] = array[len(array)-1]
    AdJustDown(array, len(array)-1)
    return max, array[0 : len(array)-1]
}


时间复杂度分析

堆排序包括两个阶段,初始化建堆和重建堆。所以堆排序的时间复杂度由这两方面组成,下面分别进行分析。

一.初始化建堆

初始化建堆只需要对二叉树的非叶子节点调用adjusthead()函数,由下至上,由右至左选取非叶子节点来调用adjusthead()函数。那么倒数第二层的最右边的非叶子节点就是最后一个非叶子结点。
  假设高度为k,则从倒数第二层右边的节点开始,这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。如果交换了,那么又要选择一支子树进行比较和交换;高层也是这样逐渐递归。
  那么总的时间计算为:s = 2^( i - 1 ) * ( k - i );其中 i 表示第几层,2^( i - 1) 表示该层上有多少个元素,( k - i) 表示子树上要下调比较的次数。
  S = 2^(k-2) * 1 + 2(k-3)*2…..+2*(k-2)+2(0)(k-1) ===> 因为叶子层不用交换,所以i从 k-1 开始到 1;
  S = 2^k -k -1;又因为k为完全二叉树的深度,而log(n) =k,把此式带入;
  得到:
S = n - log(n) -1*,所以时间复杂度为:O(n)

二.排序重建堆

在取出堆顶点放到对应位置并把原堆的最后一个节点填充到堆顶点之后,需要对堆进行重建,只需要对堆的顶点调用adjustheap()函数。
  每次重建意味着有一个节点出堆,所以需要将堆的容量减一。adjustheap()函数的时间复杂度k=log(n),k为堆的层数。所以在每次重建时,随着堆的容量的减小,层数会下降,函数时间复杂度会变化。重建堆一共需要n-1次循环,每次循环的比较次数为log(i),则相加为:log2+log3+…+log(n-1)+log(n)≈log(n!)。可以证明log(n!)和nlog(n)是同阶函数:  
  ∵(n/2)n/2≤n!≤nn,
  ∴n/4log(n)=n/2log(n1/2)≤n/2log(n/2)≤log(n!)≤nlog(n)
  所以时间复杂度为O(nlogn)

三.总结

初始化建堆的时间复杂度为O(n),排序重建堆的时间复杂度为nlog(n),所以总的时间复杂度为O(n+nlogn)=O(nlogn)。另外堆排序的比较次数和序列的初始状态有关,但只是在序列初始状态为堆的情况下比较次数显著减少,在序列有序或逆序的情况下比较次数不会发生明显变化。

你可能感兴趣的:(排序算法(1):堆排序)