堆排序之Python實現


python算法之堆排序

注意:本文中的結點和結點不加區分的使用

堆的概念:

  • 堆是一個完全二叉樹
  • 每個非葉子結點都要大於或者等於其左右孩子結點的值稱為大頂堆
  • 每個非葉子結點都要小於或者等於其左右孩子結點的值稱為小頂堆
  • 根結點一定是大頂堆中的最大值,一定是小頂堆中的最小值
    堆其實是從節點值來觀察,結點值具有一點特點的完全二叉樹

堆的類型

根據堆的特性,我們可以把堆分為兩類

  • 大頂堆
    完全二叉樹的每個非葉子結點都大於或者等於其左右孩子結點的值,根結點一定是大頂堆中的最大值,如圖1
    圖1

  • 小頂堆
    每個非葉子結點都要小於或者等於其左右孩子結點的值,根結點一定是大頂堆中的最小值,如圖2

堆排序步驟

構建完全二叉樹

原始數據:30,20,80,40,50,10,60,70,90
構建一個完全二叉樹存放數據,並根據完全二叉樹的性質5對元素編號,: 放入順序的數據結構中構造一個列表為[0,30,20,80,40,50,10,60,70,90](插入一個0,是為了將數組的下標和完全二叉樹的結點編號一致),如下圖

構建大頂堆

怎么將一個隊列構建成大頂堆(或者小頂堆),是堆排序的算法核心部分
分析
1.度數為2的結點A,如果他的左右孩子結點的最大值比它大的,最大值和該結點交換
2.度數為1的結點A,如果他的左孩子的值大於它,則交換
3.如果節點A被交換到新的位置(此時結點A已經是原來那個A的孩子結點了,所以需要A到了新的崗位上,是否能坐穩,還需要與其孩子結點比較),還需要和其他孩子結點重復上面的過程

1.構建大頂堆--起點結點的選擇
從完全二叉樹的最后一個結點的雙親結點開始,即最后一層的最右邊葉子結點的父結點開始,如果結點數為n,則起始結點的編號為n//2,這也會保證每次比較過程中,到能將所有的數都比較得到(這個堆的起始位置就是9 // 2 = 4)

2.構建大頂堆--下一個結點的選擇
從起始結點開始向左找其同層結點,到頭后再從上一層的最右邊結點開始繼續向左逐個查找,直到根結點,如下圖,進行的順序是:4,3,2,1

3.大頂堆的目標
確保每個結點的值都比左右結點的值大

可以看一個更亂的圖

排序

分析
1.將大頂堆根結點這個最大值和最后一個葉子結點交換,那么最后一個葉子結點就是最大值,將這個葉子結點排除在待排序結點之外
2.從根結點開始(新的根結點),重新調整為大頂堆后,重復上一步
問:排序,堆頂和最后一個結點交換,並排除最后一個結點,為什么要換最后一個呢?
答:堆頂的數據是已經確認了,這是最大(最小)值了,那就沒必要在留在樹中了,將其放置到最后一個葉子結點上,好標記。

總結

1.利用堆性質的一種選擇排序,在堆頂選出最大值或者最小值(這也就是可以解決我們常見的TopN問題)
2.時間復雜度為O(nlogn)
3.空間復雜度:只是使用了一個交換用的空間,所以空間復雜度為O(1)
4.堆排序是一種不穩定的排序算法

注意:由於堆排序對原始記錄的排序狀態並不敏感,因此它無論是最好、最壞,時間復雜度都是O(nlogn)

代碼實現

#!/bin/env python
# -*- coding: utf-8 -*-
'''
__title__ = ''
__author__ = 'cxding'
__mtime__ = '2020/1/3'
# code is far away from bugs with the god
'''
import math

#居中打印  數量少的可以這么打印,多了就不行了
def print_tree(array,unit_width=2):
    length = len(array)
    depth = math.ceil(math.log2(length + 1))

    index = 0

    width = 2 ** depth -1 #行寬,最深的行 15個數
    for i in range(depth):
        for j in range(2 ** i):
            #居中打印,后面追加一個空格
            print('{:^{}}'.format(array[index],width * unit_width),end=' ' * unit_width)
            index += 1
            if index >= length:
                break
        width = width // 2 #居中打印寬度減半
        print()


def sift(li:list,low:int,high:int):
    '''
    調整當前結點,這個時間復雜度最多是一棵樹的高度,所以是logn
    :param li: 列表
    :param low: 堆的根結點位置
    :param high: 堆的最后一個元素的位置
    :return:
    '''

    i = low
    j = 2 * i + 1 #j開始是左孩子

    tmp = li[low] #把堆頂存起來

    while j <= high: #只要j位置有數,沒有超過堆的長度
        if j + 1 <= high and  li[j+1] > li[j]:
            j = j + 1 # 如果有右孩子,並且他的值比左孩子大,將j指向右孩子

        if li[j] > tmp:
            li[i] = li[j]
            i = j
            j = 2 * i + 1
        else:
            li[i] = tmp
            break
    else:
        li[i] = tmp

    #return li


li = [10,40,50,30,20,90,70,80,60]


def heap(li):
    '''
    實現堆排序,這里面的時間復雜度是nlogn,所以最終的時間復雜度就是nlogn,但是這個速度會比快排慢
    :param li: 待排序的列表
    :return:
    '''
    #構建大頂堆
    n = len(li)
    print("原始的序列為:")
    print_tree(li)
    for i in range(n//2 - 1,-1,-1):
        print('-' * 30)
        sift(li,i,n-1)#high為n-1,其實是取了極大值
        print_tree(li)
    print("准備出數了!")
    #挨個出數
    for i in range(n-1,-1,-1):
       #將第一個元素和堆的最后一個元素交換
        li[0],li[i] = li[i],li[0]
        sift(li,0,i -1)#將最后一個元素排除在外,只需要調整堆頂的元素就是一個大頂堆了
        print('-'*30)
        print_tree(li)

heap(li)


再一種寫法:

#!/bin/env python
# -*- coding: utf-8 -*-
'''
__title__ = ''
__author__ = 'cxding'
__mtime__ = '2020/1/13'
# code is far away from bugs with the god
'''

origin = [0,30,20,80,50,10,60,70,90]

total = len(origin) - 1

def heap_adjust(n,i,array:list):
    '''
    調整當前結點,主要是為了保證以i為堆頂的堆是一個大頂堆(或者小頂堆)
    :param n: 待排序的序列的總長度
    :param i: 當前結點,因為要是一個堆,所以他必須至少有一個子結點
    :param array: 待排序的列表
    :return:

    '''

    while 2 * i <= n:
        lchild_index = 2 * i

        if lchild_index < n and array[lchild_index] < array[lchild_index + 1]:#如果有右孩子,並且右孩子的值比左孩子大
            #array[i],array[lchild_index + 1] = array[lchild_index + 1],array[i]
            lchild_index += 1
        if array[lchild_index] > array[i]:#接上面,已經得到了左右結點中最大結點的index,只要將其與當前結點比較,得到最大的直接就可以了,
            # 如果當前結點就是最大的,就不用比較了,這棵子樹就是大頂堆了,這是有前提的,前提是認定了,已經從len(li) // 2開始進行了一次次向上了
            array[i],array[lchild_index] = array[lchild_index],array[i]
            i = lchild_index
        else:
            break


#調整成大頂堆
for i in range(total//2,0,-1):
    heap_adjust(total,i,origin)

'''

heap_adjust(total,4,origin)
print(origin)

heap_adjust(total,3,origin)
print(origin)

heap_adjust(total,2,origin)
print(origin)

heap_adjust(total,1,origin)
print(origin)
#[0, 90, 50, 80, 30, 10, 60, 70, 20]
'''
print(origin)
#[0, 90, 50, 80, 30, 10, 60, 70, 20]

def heap(max_heap:list):
    length = len(max_heap) - 1
    print("length ---> ",length)
    print("origin--->",max_heap)
    for i in range(length,1,-1):
        print("i-->",i,"element--",max_heap[i])
        max_heap[1],max_heap[i]= max_heap[i],max_heap[1]
        heap_adjust(i-1,1,max_heap)

heap(origin)
print(origin)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM