排序算法-堆排序

排序算法-堆排序

二叉堆是一棵完全二叉树,堆中的子节点总是不大于父节点的值
用数组存储二叉堆的时候,以堆的最大元素作为数组index为1,则对节点为i而言其左孩子在数组中的index为2*i,右孩子节点的Index为2 * i + 1

1. 最大堆的建立

最大堆的存储结构为:根节点是所有元素中最大的一个,且其堆中所有子节点均不大于父节点的值,向已有最大堆里边新增一个元素的具体步骤为:
(1)首先向数组末端加入一个元素,即最大堆的最后一个叶子节点;
(2)将新增的叶子节点与其父节点比较大小,如果小于父节点,则不进行任何操作,否则将该节点与父节点交换值;
(3)依据步骤(2)逐级向上递推,直至根节点位置。

2.从最大堆中删除某个元素

一般而言,从最大堆中删除元素都是删除最大堆的根节点,然后将根节点与原始堆的最后一个叶子节点交换值,然后将根节点值与其左右孩子比较值大小,与较大的值交换元素值;交换一次之后,比较交换之后孩子的孩子与其值的大小,依次递推,直至到最后一个子节点位置结束,此时堆已经满足了最大堆的性质。

class MaxHeap(object):
    def __init__(self):
        self.heap = [0]
        # 此处count可设置为0,设置为0的时候需要注意边界值的判断修改
        self.count = 1

    def is_empty(self):
        return self.count == 1

    # 堆的heap_push操作为对于每一个新插入的子节点,其节点位置必须为二叉树的最后一个节点,则其父节点的位置为 position_k // 2
    # 如果该位置的元素大于父节点,则与父节点进行交换位置,注意:此时堆的index从1开始计数
    def __heap_push(self, position_k):
        # 对于第一个元素不需要判断是否需要上移动操作,直接从position_k=2的位置开始
        while position_k > 1 and self.heap[position_k // 2] < self.heap[position_k]:
            self.heap[position_k // 2], self.heap[position_k] = self.heap[position_k], self.heap[position_k // 2]
            # 交换值之后,需要将position的位置设置为父节点的位置
            position_k = position_k // 2

    # 堆的heap_down操作为对堆中的元素进行删除操作,删除根节点之后,将最后一个子节点移动到第一个子节点的位置
    # 二叉树仍然满足完全二叉树的结构,但是就不再满足最大堆的结构,需要调整根节点的位置
    # 对根节点需要进行下移操作:需要比较根节点与左右节点的大小,选择与三个元素里边最大的进行交换位置
    def __heap_down(self, position_k):

        # 循环终止的条件为:已经移动到某个叶子节点,即被删除元素的位置没有子节点
        # 如果有左孩子则存在孩子,注意我们这里的count从1开始计数,所以只能取到count-1的值
        while 2 * position_k < self.count:
            left_child = 2 * position_k
            # 如果右孩子存在,且大于左孩子的值,则将根节点下沉到右孩子位置
            if left_child + 1 < self.count and self.heap[left_child + 1] > self.heap[left_child]:
                left_child += 1

            # 如果根节点大于左孩子的值,则直接跳出循环,否则交换根节点与左孩子的位置
            if self.heap[left_child] <= self.heap[position_k]:
                break

            self.heap[left_child], self.heap[position_k] = self.heap[position_k], self.heap[left_child]

            # 将待下沉的位置移动到下一个元素
            position_k = left_child

    # 一般堆插入元素都是首先插入数组的末尾,再进行上浮的操作
    def insert(self, element):
        self.heap.append(element)
        self.count += 1
        self.__heap_push(self.count - 1)

    # 一般堆删除元素都是删除根节点的位置,即最大堆的最大元素,然后将最后一个元素与根节点元素进行交换,然后对根节点进行下沉操作
    def extract(self):
        # 保证堆中存在元素
        assert self.count > 0
        # 交换堆的根节点与最后一个元素
        return_item = self.heap[1]
        self.heap[1] = self.heap[-1]

        # 删除最后一个元素
        self.heap.pop()
        self.count -= 1
        self.__heap_down(1)

        return return_item
3.堆排序操作

堆排序:时间复杂度O(NlogN),空间复杂度O(N)

# 堆排序:时间复杂度O(NlogN),空间复杂度O(N)
def heap_sort(input_arr):
    heap = MaxHeap()

    # 首先建立堆
    for i in range(0, len(input_arr)):
        heap.insert(input_arr[i])

    print(heap.heap)

    # 其次依次从堆中取出元素
    for i in range(len(input_arr)-1, -1, -1):
        input_arr[i] = heap.extract()
    print(input_arr)

    return input_arr

4. heapify构造堆

heapify构造堆与上述创建堆的步骤不一致之处在于,从叶子节点出发开始创建堆,首先对于输入数据全部输入到构造堆的空间,然后对最后一个父节点开始进行heap-down下沉操作,逐次地再对上述的父节点进行下沉操作,下沉操作与上述一致。

heapify堆排序,快于第一个堆排序的过程,第一个排序过程每次都是插入数据,插入的时间复杂度为O(NlogN)

# heapify构造堆
class Heapify(object):
    def __init__(self, input_arr):

        self.heap = [0]
        for i in range(0, len(input_arr)):
            self.heap.append(input_arr[i])
        self.count = len(input_arr) + 1

        # 对最后一个父节点开始进行heap_down的操作
        for i in range(self.count // 2, 0, -1):
            self.__heap_down(i)

    def is_empty(self):
        return self.count == 1

    # 堆的heap_down操作为对堆中的元素进行删除操作,如果删除的是某个父节点则其删除之后,将最后一个子节点移动到第一个子节点的位置
    # 二叉树仍然满足完全二叉树的结构,但是就不再满足最大堆的结构,需要调整根节点的位置
    # 对根节点需要进行下移操作:需要比较根节点与左右节点的大小,选择与三个元素里边最大的进行交换位置
    def __heap_down(self, position_k):

        # 循环终止的条件为:已经移动到某个叶子节点,即被删除元素的位置没有子节点
        # 如果有左孩子则存在孩子
        while 2 * position_k < self.count:
            left_child = 2 * position_k
            # 如果右孩子存在,且大于左孩子的值,则将根节点下沉到右孩子位置
            if left_child + 1 < self.count and self.heap[left_child + 1] > self.heap[left_child]:
                left_child += 1

            # 如果根节点大于左孩子的值,则直接跳出循环,否则交换根节点与左孩子的位置
            if self.heap[left_child] <= self.heap[position_k]:
                break

            self.heap[left_child], self.heap[position_k] = self.heap[position_k], self.heap[left_child]

            # 将待下沉的位置移动到下一个元素
            position_k = left_child

    # 一般堆删除元素都是删除根节点的位置,即最大堆的最大元素,然后将最后一个元素与根节点元素进行交换,然后对根节点进行下沉操作
    def extract(self):
        # 保证堆中存在元素
        assert self.count > 0
        # 交换堆的根节点与最后一个元素
        return_item = self.heap[1]
        self.heap[1] = self.heap[-1]

        # 删除最后一个元素
        self.heap.pop()
        self.count -= 1
        self.__heap_down(1)

        return return_item
        
# 堆的叶子节点都是最大值,第一个非叶子节点为count // 2, 从索引1开始计算
# heapify过程算法时间复杂度为O(N),但是排序过程还是O(NlogN)
# 堆更多的是用于动态数据的维护
def heapify_sort(input_arr):
    heap = Heapify(input_arr)

    # 其次依次从堆中取出元素
    for i in range(len(input_arr) - 1, -1, -1):
        input_arr[i] = heap.extract()
    return input_arr

5.原地堆排序

根据上述构造堆的过程可以知道,构造堆的时候需要额外的开辟O(N)的空间,下面介绍一种原地堆排序的方法,不需要额外的开辟空间,直接对待排序的序列进行操作。

其基本思想是:每次都将最大的元素移动到数组的最后一个位置

# 原地堆排序:时间复杂度O(NlogN),空间复杂度O(1),不需要另外开辟空间
# 每次都将最大的元素移动到数组的最后一个位置,由于数组的位置从0开始,所以在索引左孩子,右孩子的时候需要在原始1的基础上进行调整
# parent(i) = (i-1) / 2, 左孩子:2 * i + 1, 右孩子 2 * (i + 1)
# 第一个非叶子节点 (count - 1)// 2
# 原地堆排序的heap_down的操作,由于不另外开辟空间,所以直接对序列进行操作
def __heap_down(input_arr, input_length, position_k):
    count = input_length

    # 循环终止的条件为:已经移动到某个叶子节点,即被删除元素的位置没有子节点
    # 如果有左孩子则存在孩子, 左孩子不越界
    while 2 * position_k + 1 < count:
        left_child = 2 * position_k + 1
        # 如果右孩子存在,且大于左孩子的值,则将根节点下沉到右孩子位置, 右孩子不越界
        if left_child + 1 < count and input_arr[left_child + 1] > input_arr[left_child]:
            left_child += 1

        # 如果根节点大于左孩子的值,则直接跳出循环,否则交换根节点与左孩子的位置
        if input_arr[left_child] <= input_arr[position_k]:
            break

        input_arr[left_child], input_arr[position_k] = input_arr[position_k], input_arr[left_child]

        # 将待下沉的位置移动到下一个元素
        position_k = left_child


def position_heap_sort(input_arr):
    input_length = len(input_arr)

    # heapify 构建堆
    for i in range((input_length - 1) // 2, -1, -1):
        __heap_down(input_arr, input_length, i)

    for i in range(input_length-1, -1, -1):
        # 将当前最大的元素放置到合适的位置
        input_arr[0], input_arr[i] = input_arr[i], input_arr[0]

        # heap_down的元素个数为i
        __heap_down(input_arr, i, 0)

    return input_arr
6. 索引堆

索引堆:指的是在原来输入数据的基础上,不改变数据在数组中的位置,重新建立一个索引值,所有的堆的上浮下沉操作只对索引值进行操作。

example:
原始输入数据为: [46, 11, 25, 29, 49, 45, 9, 27, 29, 22]
构造最大堆后,输入数据值不变,变化的是其索引值:
input_data: [0, 46, 11, 25, 29, 49, 45, 9, 27, 29, 22]
heap_index: [0, 5, 1, 6, 9, 4, 3, 7, 2, 8, 10] (注意:此处index从1开始计算,因为堆的索引一般从1开始计数,所以为最开始位置赋予0值)
根据heap_index索引获取可得到构造的最大堆为:
max_heap_data: [0, 49, 46, 45, 29, 29, 25, 9, 11, 27, 22]

此处代码先可以忽略reverse变量的创建,及其修改,其作用是为了后续修改堆中指定位置元素而创建

# 如果堆中的元素为长字符串的时候,那么要进行交换消耗较大
# 构建堆的时候,只有索引的位置发生改变
# 比较的时候比较的是元素位置,但是交换的时候变换的是index
class IndexMaxHeap(object):
    def __init__(self):
        self.heap = [0]
        self.count = 0
        self.index_heap = [0]
        self.reverse = [0]

    def is_empty(self):
        return self.count == 0

    # 执行最大堆的建立操作,每次都与父亲节点进行比较,当值大于父亲节点的时候才进行交换操作
    def __heap_push(self, position_k):

        while position_k > 1 and self.heap[self.index_heap[position_k // 2]] < self.heap[self.index_heap[position_k]]:
            self.index_heap[position_k // 2], self.index_heap[position_k] = self.index_heap[position_k], \
                                                                                self.index_heap[position_k // 2]

            self.reverse[self.index_heap[position_k // 2]] = position_k // 2
            self.reverse[self.index_heap[position_k]] = position_k

            position_k = position_k // 2

    # heap_down操作主要是与左右孩子进行比较大小,然后进行索引值的交换操作等
    # 注意:此时我们的堆的索引值从1开始计数
    def __heap_down(self, position_k):

        # 当存在左右节点的时候,即左孩子不越界, self.count 值是堆的最后一个元素
        while position_k * 2 <= self.count:
            left_child_index = position_k * 2

            # 当右孩子存在,则进行右孩子与左孩子的比较
            if position_k * 2 + 1 <= self.count and \
                    self.heap[self.index_heap[left_child_index + 1]] > self.heap[self.index_heap[left_child_index]]:
                left_child_index += 1

            # 当要交换的节点元素小于原始元素值的时候,直接跳出循环
            if self.heap[self.index_heap[position_k]] >= self.heap[self.index_heap[left_child_index]]:
                break

            # 否则将索引堆的索引值进行下沉操作
            self.index_heap[left_child_index], self.index_heap[position_k] = \
                self.index_heap[position_k], self.index_heap[left_child_index]

            self.reverse[self.index_heap[position_k]] = position_k
            self.reverse[self.index_heap[left_child_index]] = left_child_index

            position_k = left_child_index

    # 对于外部用户而言,index索引值从零开始
    def insert(self, item):

        self.index_heap.append(len(self.index_heap))
        self.heap.append(item)
        self.count += 1
        self.reverse.append(self.count)
        self.__heap_push(self.count)

    # 对于外部用户而言,index索引值从零开始
    def extract(self):

        ret = self.heap[self.index_heap[1]]
        self.index_heap[1] = self.index_heap[-1]
        self.index_heap.pop()
        self.count -= 1
        self.__heap_down(1)

        return ret

    # 对于外部用户而言,index索引值从零开始
    def extract_max_index(self):

        ret = self.index_heap[1] - 1

        self.index_heap[1], self.index_heap[-1] = self.index_heap[-1], self.index_heap[1]
        self.reverse[1], self.reverse[-1] = self.reverse[-1], self.reverse[1]

        self.index_heap.pop()
        self.count -= 1

        self.__heap_down(1)

        return ret

    def contain(self, index_i):
        assert 1 < index_i + 1 <= self.count
        return self.reverse[index_i + 1] != 0

    def get_item(self, index_i):
        assert(self.contain(index_i))
        return self.heap[index_i + 1]

    # 将索引为index_id的值修改为一个新的item
    def item_change(self, index_i, new_item):

        # 检查index_i的合法性
        assert self.contain(index_i)

        index_i += 1
        self.heap[index_i] = new_item

        # 数据发生改变之后,为了维持堆的性质,需要进行堆的操作
        # 首先需要找到heap在堆中的位置(遍历查找的时间复杂度为O(N)), index_heap[k] = index_i,之后进行上移动和下沉操作
        # for j in range(1, self.count):
        #     if self.index_heap[j] == index_i:
        #         self.__heap_push(j)
        #         self.__heap_down(j)
        #         return

        # 可以使用reverse_index
        j = self.reverse[index_i]
        self.__heap_push(j)
        self.__heap_down(j)

7. 修改索引堆中指定元素

继续该例子,如果想要指定位置

example:
原始输入数据为: [46, 11, 25, 29, 49, 45, 9, 27, 29, 22]
如果我们想要修改索引堆的第9个元素的值为70,即将29的值修改成70
按照常规操作其步骤为:
(1)将该元素的值修改成70之后,先对该元素值进行heap_push,然后进行heap_down的操作即可进行修改;
(2)问题的关键在于找到29对应元素在最大堆中的位置,根据heap_index,我们可以知道原来29元素在最大堆的第4个位置(数据索引9在heap_index中的位置);
(3)问题的主要又变成了找到heap_index中指定修改数据索引的位置,最简单的方法即为直接遍历heap_index,找到find_index-9的位置即可。

 for j in range(1, self.count):
     if self.index_heap[j] == index_i

该方法查找的时间复杂度是O(N),能否找到一个更好的方法来替代该方法进行查找,将时间复杂度变成O(1)呢?
答案就在于上述代码中的新建的变量reverse,去构建最大堆索引值与其位置之间的一个映射。

就上述例子而言,其值为:
input_data: [46, 11, 25, 29, 49, 45, 9, 27, 29, 22]
max_heap: [0, 49, 46, 45, 29, 29, 25, 9, 11, 27, 22]
index_heap: [0, 5, 1, 6, 9, 4, 3, 7, 2, 8, 10]
reverse: [0, 2, 8, 6, 5, 1, 3, 7, 9, 4, 10]
reverse的具体含义为:
2: index_heap中1 的位置,
8: index_heap中2 的位置,
6: index_heap中3 的位置,
5: index_heap中4 的位置,

reverse与index_heap之间的关系为:
index_heap[i] = j, reverse[j] = i
index_heap[reverse[j]] = j
reverse[index_heap[i]] = i

然后就可以使用reverse[index_i]获取需要修改元素的位置值,对其进行上浮下沉操作。

每天进步一点点…上述代码均已通过小样本测试用例。

如若上述表述有误,还望指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值