zoukankan      html  css  js  c++  java
  • 堆排序

    此次分享的堆排序实现代码是没有使用递归的,在开始之前,要先补给几个知识。

    知识补给1:完全二叉树

    满二叉树:一个二叉树,如果每一层的节点数都达到最大值,则这个二叉树就是满二叉树。

     在上图中,只有(a)是满二叉树。

    完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。

    (也就是说,相对比满二叉树而言,他可以缺少子节点,但是只能从最后按顺序缺)

    在上图中,b、c、d都缺少子节点,但b是在最底层而且从最右边开始缺,所以b是完全二叉树。

    而我们的推排序是一种特殊的完全二叉树!!!!!

    知识补给2:二叉树的顺序储存方式

    二叉树的存储方式(表示方式)有两种

    • 链式存储方式
    • 顺序存储方式(列表)

    而堆排序用的是二叉树的顺序存储方式。

    我们来看一下这颗完全二叉树里面存储的数据的下标规律:

    它是根据列表从左到右的顺序存储到树里面的。这样我们就可以观察一下父节点和子节点的下标关系了

    父节点和左孩子节点的关系(节点的值:下标)

      9 - 8 : 0 - 1  8 - 6 : 1 - 3  6 - 2 : 3 - 7

    用数学归纳法可以发现规律: i = 2i + 1

     父节点和右孩子节点的关系(节点的值:下标)

     9 - 7:0 -  2     8 - 5:1 - 4  7 - 1:2 - 6

    用数学归纳法可以发现规律: i = 2i + 2

    相同的,我们可以根据子节点的下标反推出父节点的下表:(i - 1) // 2

    知识补给3:什么是堆

    堆:一种特殊的完全二叉树结构

    大根堆:一颗完全二叉树,满足任一节点都比其孩子节点大

    小根堆:一颗完全二叉树,满足任一节点都比其孩子节点小

     

    知识补给4:堆的向下调整性质

    • 假设根节点的左右子树都是堆,但根节点不满足堆的性质(大根堆,小根堆)
    • 可以通过一次向下的调整来将其变成一个堆

     堆排序过程:

    1. 建立堆
    2. 得到堆顶元素,为最大元素
    3. 去掉堆顶,将堆最后一个元素放到堆顶,此时可通过一次调整重新使堆有序。
    4. 堆顶元素为第二大元素
    5. 重复步骤3,直到堆变空

    构造堆(农村包围城市)

    堆排序的实现过程中,其实大部分过程都在调整过程(建立堆,排序的时候去掉堆顶,重新调整)

    所以我们先来实现调整堆的代码

    def sift(li, low, high):
    """
    调整堆的顺序,使其称为大根堆or小根堆
    :param li: 要排序的列表
    :param low: 堆的根节点的位置
    :param high: 堆最后一个元素的位置(边界)
    :return:
    """
    # 首先要定义两个变量来记录父节点和子节点的位置
    i = low # 父节点
    j = 2 * i + 1 # 左孩子节点
    tmp = li[low] # 根节点的值

    while j <= high: # 只要不超过边界就一直与子节点比较大小进行调整

    if j + 1 <= high and li[j + 1] > li[j]: # 如果右边节点存在且值比左边大,就把j指向右边的值的下标
    j = j + 1
    if li[j] > tmp: # 如果子节点的值大于父节点,就互相替换,然后进入下一层
    li[i] = li[j]
    i = j
    j = 2 * i + 1
    else:
    # tmp的值比子节点的值大,直接跳出循环,下面的子树也不用比较了,因为下面的树是已经有序了的,例如(第一层无序,调整完位置之后,tmp都比第二层的子节点大,此时第二层的父节点是i,就把tmp赋值给i)
    # 因为构建堆的时候是农村包围城市的,即从最靠后面的子树开始建堆的
    li[i] = tmp
    break
    else:
    # 超出了边界,直接替换子节点
    li[i] = tmp

    写完这个调整位置的函数之后,我们实现堆排序的代码就很简单了。

    def heap_sort(li):
        """
        实现堆排序过程:
            1、先根据传入的列表构建堆(大根堆)
            2、再埃个出数
        :param li: 
        :return: 
        """
        n = len(li)
        
        # 1、构建堆--农村包围城市
        for i in range((n-2)//2, -1, -1):
            # i 就是每个父节点的位置
            # (n-2)//2 是根据堆的最后一个叶子节点位置推算其父节点位置的公式:(i-1)//2
            # 而 i 又等于 n - 1, 所以 (i-1)//2 == ((n - 1)-1)//2  ==  (n-2)//2 
            # 第一个 -1 ,因为要从最后一个根节点循环到第一个根节点也就是列表中下标为0的元素,range 为开头闭尾
            # 第二个 -1, 倒序
            sift(li, i, n-1)
        
        # 2、埃个出数,完成列表排序,
        # 原地排序,每次都根节点都和最后一个叶子节点互换,然后调整位置, 再进行如上循环操作,直到堆为空
        for i in range(n-1, -1, -1):
            # i是每次循环堆里面最后一个数
            li[0], li[i] = li[i], li[0]
            sift(li, 0, i-1)        # 边界值下标, 因为出数完之后,最后原i位置上的元素是出数的结果,所以i位置上的元素不参加调整,故边界值为i-1.

    到此,堆排序的代码就实现完成了,我们可以来简单测试一下。

    li = [i for i in range(100)]
    import random
    random.shuffle(li)
    print(li)
    heap_sort(li)
    print(li)
    
    
    [26, 16, 81, 29, 11, 75, 73, 0, 35, 21, 95, 37, 72, 79, 80, 46, 76, 1, 93, 45, 25, 48, 92, 77, 42, 40, 82, 10, 6, 67, 30, 96, 47, 51, 38, 60, 4, 61, 64, 97, 33, 71, 8, 59, 87, 49, 19, 22, 83, 44, 9, 28, 27, 99, 69, 12, 2, 34, 24, 85, 32, 53, 14, 88, 90, 41, 55, 39, 20, 94, 63, 23, 7, 66, 84, 74, 62, 58, 56, 70, 86, 17, 89, 13, 18, 65, 50, 98, 5, 54, 15, 78, 3, 57, 31, 52, 68, 43, 36, 91]
    
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

    堆排序-- topk问题

    现在有n个数,设计算法得到前k大的数。 (k<n)

    解决思路:

    • 排序后切片  O(nlogn)   切片时间忽略不计
    • 排序lowb三人组          O(kn)
    • 堆排序思路          O(nlogk)   n个数进来堆比较logk次,
    # 实现思路
    # 1、创建一个小根堆
    # 2、循环列表里面的数,和小根堆的根值比较,如果比根值要大,则替换小根堆的根值,然后再调整一次根堆
    # 3、出数
    
    def sift(li, low, high):
        """
        调整位置
        :param li:
        :param low: 根堆的小的值的位置
        :param high: 根堆的边界值
        :return:
        """
        i = low
        tmp = li[low]
        j = 2 * i + 1  # 左边孩子的下标
    
        while j <= high:  # 只要不超过边界值 父节点和子节点就会继续相比较
    
            if j + 1 <= high and li[j + 1] < li[j]:  # 如果j+1没有超过边界值,且比j对应的值小, j则指向j+1
                j = j + 1
            if li[j] < tmp:
                li[i] = li[j]
                i = j # 进入下一层
                j = 2 * i + 1
            else:
                li[i] = tmp
                break
        else:
            li[i] = tmp
    
    
    def topk(li, num):
        head = li[0: num]
    
        # 1、构建小根堆 (i-1)//2
        for i in range((num - 2) // 2, -1, -1):
            # i 表示每一根节点的位置
            sift(head, i, num-1)
    
        # 2、循环列表每一个数与小根堆的根值进行比较
        for i in range(num, len(li)):
            if li[i] > head[0]:
                head[0] = li[i]
                sift(head, 0, num-1)
    
        # 3、出数
        for i in range(num-1, -1, -1):
            # i 表示最后一个数的位置
            head[0], head[i] = head[i], head[0]
            sift(head, 0, i-1)
        return head
    
    li = [i for i in range(1000)]
    import random
    random.shuffle(li)
    print(topk(li, 100))
  • 相关阅读:
    bzoj 1856 组合
    bzoj 2809 左偏树平衡树启发式合并
    【HMOI】小C的填数游戏 DP+线段树维护
    【HNOI】 小A的树 tree-dp
    bzoj 1483 链表启发式合并
    bzoj 2733 平衡树启发式合并
    bzoj 2669 状压DP
    bzoj 2165 DP
    【HNOI】 lct tree-dp
    远程debug配置
  • 原文地址:https://www.cnblogs.com/Xuuuuuu/p/10803327.html
Copyright © 2011-2022 走看看