【数据结构与算法】最适合新手小白的教程——两种进阶排序算法+变个“魔术”(包你看懂!)

hello大家好!这两天忙着做前端去了,没怎么更新博客,正好今天去趟医院,就接着往下写一篇,机器学习的内容我下一篇文章应该会更新~

这篇文章大概讲两种“不一样的算法”,桶排序和堆排序,可能确实不太好理解,会比前面的稍微复杂一点,看不懂的朋友可以去哔站搜搜教程或者多看遍(我相信我应该还是可以讲明白的哈哈哈)然后就是“魔术方法”(Python里面好像是这么叫的啊,感觉还蛮贴切的,java里面叫比较器,c++里面叫函数重载)

好啦,咱们话不多说,直奔主题!开卷了同志们~

1、堆排序

堆排序是一种基于堆数据结构的比较排序算法。堆是一棵完全二叉树,分为最大堆和最小堆(大根堆和小根堆)最大堆的每个节点的值都大于或等于其子节点的值,最小堆的每个节点的值都小于或等于其子节点的值。堆排序一般采用最大堆。(优先级队列结构就是堆结构,堆顶就是优先级最大的地方)

#堆数据结构是基于完全二叉树的一种特殊树形结构,但并不是所有的完全二叉树都是堆。堆需要满足以下两个条件:

        (1)完全二叉树:堆必须是完全二叉树,即除了最后一层外,每一层的所有节点都有两个子                   节点,最后一层的节点尽可能集中在左边。   

        (2)是最大堆(在最大堆中,父节点的值总是大于或等于其子节点的值)或者最小堆(在最                   小堆中,父节点的值总是小于或等于其子节点的值)。

我们画图来看看完全二叉树:

#图中只有打叉的不是完全二叉树

再来看看大根堆的示例:

        10
       /  \
      9    8
     / \  / \
    7  6 5   4

# 满足父节点大于其两个子节点

小根堆就与其相反:

        1
       /  \
      2    3
     / \  / \
    4  5 6   7

# 满足父节点小于子节点

那么,如何将实现堆结构,进行堆排序呢?

堆排序主要包括两个步骤:

  1. 构建最大堆:将无序数组构建为最大堆。
  2. 堆排序:将最大堆的根节点(最大值)与最后一个节点交换,然后对前面的n-1个节点重新调整为最大堆,重复这个过程直到整个数组有序。

我们先看看代码:

def heapify(arr, n, i):
    """
    将一个数组调整为堆。
    
     arr: 待调整的数组
     n: 数组的长度
     i: 当前节点的索引
    """
    largest = i  # 初始化最大值为根节点
    left = 2 * i + 1  # 左子节点索引
    right = 2 * i + 2  # 右子节点索引

    # 如果左子节点存在且大于根节点,则更新最大值
    if left < n and arr[left] > arr[largest]:
        largest = left

    # 如果右子节点存在且大于当前最大值,则更新最大值
    if right < n and arr[right] > arr[largest]:
        largest = right

    # 如果最大值不是根节点,需要交换并继续调整
    if largest != i:
        arr[i], arr[largest] = arr[largest], arr[i]  # 交换
        heapify(arr, n, largest)  # 递归调整子树

def heap_sort(arr):
    """
    对数组进行堆排序。
    
    arr: 待排序的数组
    """
    n = len(arr)

    # 构建最大堆
    for i in range(n // 2 - 1, -1, -1):
        heapify(arr, n, i)

    # 逐步将最大元素移动到数组末尾,并重新调整堆
    for i in range(n - 1, 0, -1):
        arr[i], arr[0] = arr[0], arr[i]  # 将当前最大元素(根节点)移到数组末尾
        heapify(arr, i, 0)  # 重新调整剩余元素为堆

#调用
heap_sort(arr)

#对于i位置的父节点来说,他左边的子节点是2*i+1,右边的子节点是2*1+2,i是这个数在数组中的位置,通过这种索引的变化,完成了从数组到二叉树的转换

#heapify函数用于将一个数组调整为堆。它会检查当前节点和其子节点,找到最大的节点并交换,然后递归地调整子树。

#heap_sort函数首先构建一个最大堆,然后逐步将最大值(根节点)移动到数组末尾,并对剩下的元素重新调整为堆。

#在构建最大堆时,从最后一个非叶子节点开始,向前调整。

#在排序过程中,每次将根节点(最大值)移动到数组末尾,然后对剩余部分进行调整。

#非叶子节点的意思是指那些至少有一个子节点的节点。

#在堆这种完全二叉树中,非叶子节点出现在数组的前半部分。所以第一个for循环的位置是:n // 2 - 1

举个例子:

         12
       /    \
      11    13
     /  \   /
    5    6 7


# 其中13就是最后一个非叶子节点
# 叶子节点:5, 6, 7(没有任何子节点)
# 非叶子节点:12, 11, 13(至少有一个子节点)

#数组在经过第一个for循环后,调整为大根堆结构,最大的数保证在第一个。排序时只要将数组中最后一个数与第一个数交换,整个堆的最大值就到了数组末尾,将二叉树的范围减少,不考虑那个最大的数(此时这个数应该是排好序的了),接着将前面的部分变成大根堆,循环递归这些操作,从上往下调整堆,每次踢出一个最大的数,最终数组有序。

给大家举个例子;

#这块内容不太好理解,大家一定要多想多画图,然后再实操。

#这里就先不放例题了,大家感兴趣能理解的话可以去别的博主哪里看一下。

#就给个经典的题目吧,大家可以闲着没事情想一想:

已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法来对数组排序

(提示:对于几乎有序的数组,最适合的排序算法是基于最小堆的排序算法。这是因为每个元素移动的距离不超过 k,我们可以使用大小为 k+1 的最小堆(可以用Python自带的,这样会方便些)来帮助我们完成排序。)

想了想还是写一下堆结构,这个还是蛮重要的,比排序还重要

先给个构造最大堆的代码:

class MaxHeap:
    def __init__(self):
        # 初始化一个空的堆列表
        self.heap = []

    def insert(self, element):
        """
        插入元素到堆中。

         element: 要插入的元素
        """
        # 将新元素添加到堆的末尾
        self.heap.append(element)
        # 向上调整堆,以保持最大堆的性质
        self._heapify_up(len(self.heap) - 1)

    def delete_max(self):
        """
        删除并返回堆顶(最大)元素。

         return: 堆顶元素,如果堆为空则返回 None
        """
        if len(self.heap) == 0:
            return None

        if len(self.heap) == 1:
            # 如果堆中只有一个元素,直接弹出并返回
            return self.heap.pop()

        # 交换堆顶元素和最后一个元素
        self._swap(0, len(self.heap) - 1)
        # 弹出并保存堆顶元素
        max_element = self.heap.pop()
        # 向下调整堆,以保持最大堆的性质
        self._heapify_down(0)
        return max_element

    def get_max(self):
        """
        获取堆顶(最大)元素。

         return: 堆顶元素,如果堆为空则返回 None
        """
        if len(self.heap) == 0:
            return None
        return self.heap[0]

    def _heapify_up(self, index):
        """
        向上调整堆,以保持最大堆的性质。

         index: 当前元素的索引
        """
        parent_index = (index - 1) // 2  # 从当前节点往上推
        # 如果当前元素大于其父节点,则交换并递归向上调整
        if index > 0 and self.heap[index] > self.heap[parent_index]:
            self._swap(index, parent_index)
            self._heapify_up(parent_index)

    def _heapify_down(self, index):
        """
        向下调整堆,以保持最大堆的性质。

         index: 当前元素的索引
        """
        left_child_index = 2 * index + 1
        right_child_index = 2 * index + 2
        largest = index

        # 找到左、右子节点中较大的那个
        if left_child_index < len(self.heap) and self.heap[left_child_index] > self.heap[largest]:
            largest = left_child_index
        if right_child_index < len(self.heap) and self.heap[right_child_index] > self.heap[largest]:
            largest = right_child_index

        # 如果当前节点不是最大的,则交换并递归向下调整
        if largest != index:
            self._swap(index, largest)
            self._heapify_down(largest)

    def _swap(self, i, j):
        """
        交换堆中的两个元素。

         i: 第一个元素的索引
         j: 第二个元素的索引
        """
        self.heap[i], self.heap[j] = self.heap[j], self.heap[i]

# 示例使用
heap = MaxHeap()
heap.insert(12)
heap.insert(11)
heap.insert(13)
heap.insert(5)
heap.insert(6)
heap.insert(7)

print("堆顶元素:", heap.get_max())  # 输出 13
print("删除堆顶元素:", heap.delete_max())  # 输出 13
print("删除后的堆顶元素:", heap.get_max())  # 输出 12

#打字太麻烦了,这里偷个懒,让AI帮我说一下QAQ

#其实你只要看懂了前面说的排序,后面的结构其实是大差不差的,我在代码里面的注释已经挺详细的了,这里就不在赘述。主要需要掌握的就是插入方法和向上向下调整堆结构的方法

2、桶排序

桶排序(Bucket Sort)是一种基于分布的排序算法。它将元素分配到多个桶中,然后对每个桶内的元素进行排序,最后合并所有桶内的元素得到有序的数组。桶排序适用于数据均匀分布的情况,并且可以在某些特定情况下达到线性时间复杂度 O(n)。

这里来区别一下,什么是基于分布,什么是基于比较:

基于比较的排序算法:基于比较的排序算法是通过比较元素之间的大小关系来实现排序的。这类算法在排序过程中会多次比较元素,并根据比较结果来交换元素的位置。如冒泡排序、选择排序、插入排序、归并排序、快速排序、堆排序。

基于分布的排序算法:基于分布的排序算法不依赖于元素之间的比较,而是利用元素的某些特征(如数值的分布)来实现排序。这类算法通常会根据元素的值将它们分配到特定的桶或位置,然后再进行合并或整理。如:计数排序、桶排序、基数排序。

举个例子:假设我们有大量正整数表示的IP地址(IPv4地址可以转换为32位无符号整数)。我们需要对这些IP地址进行排序,这时就可以使用基数排序。

回到正题,我们接着介绍桶排序

桶排序的步骤

  1. 创建桶:创建一个定量的数组(或列表)作为桶。
  2. 分配元素:遍历输入数据,将每个元素放入对应的桶中。
  3. 桶内排序:对每个桶中的元素进行排序(可以使用任何排序算法,如插入排序、快速排序等)。
  4. 合并桶:将所有桶中的元素按顺序合并,得到排序后的数组。

下面是代码展示:

def bucket_sort(arr):
    """
    使用桶排序算法对输入数组进行排序。

     arr: 需要排序的数组
     return: 排序后的数组
    """
    if len(arr) == 0:
        return arr

    # 1. 创建桶
    # 找到数组中的最大值和最小值
    min_value, max_value = min(arr), max(arr)
    # 确定桶的数量
    bucket_count = len(arr)
    # 初始化每个桶,创建了一个包含 bucket_count 个空列表的列表,每个空列表代表一个桶
    buckets = [[] for _ in range(bucket_count)]

    # 2. 分配元素到桶
    for num in arr:
        # 使用简单的映射函数将元素分配到相应的桶(将 num 相对于最小值的偏移量归一化,得到一个在 
        #[0, 1) 之间的比值,表示 num 在整个范围中的相对位置。将归一化的比值放大到 [0, 
        #bucket_count) 范围,得到 num 应该放置在哪个桶中的浮点数索引。)
        index = int((num - min_value) / (max_value - min_value + 1) * bucket_count)
        buckets[index].append(num)

    # 3. 桶内排序
    for i in range(bucket_count):
        # 可以使用任意的排序算法,这里使用内置的 sorted() 函数
        buckets[i] = sorted(buckets[i])

    # 4. 合并桶
    sorted_arr = []
    for bucket in buckets:
        sorted_arr.extend(bucket)

    return sorted_arr

# 示例使用
sorted_arr = bucket_sort(arr)

#对每个桶中的元素进行排序。这里使用了 Python 内置的 sorted() 函数,可以替换为其他排序算法。

#初始化一个空列表 sorted_arr,依次将每个桶中的元素合并到 sorted_arr 中,得到排序后的数组。

# 这里的映射函数关系要按照实际情况更改,来改变每个元素对应的桶。       

#按照上述代码,我们可以假设一个实例:假设我们有一组学生的成绩,范围在 0 到 1 之间(表示百分比),需要将这些成绩进行排序。

输入数组arr = [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68]

找到最大值和最小值min_value = 0.12max_value = 0.94

确定桶的数量bucket_count = 10(等于数组的长度)。

初始化桶

buckets = [[], [], [], [], [], [], [], [], [], []]

分配元素到桶

        对于 num = 0.78,计算过程如下:

index = int((0.78 - 0.12) / (0.94 - 0.12 + 1) * 10)
     = int(0.66 / 1.82 * 10)
     ≈ 3

        0.78 放入桶索引为 3 的桶中。

重复上述过程,将所有元素分配到相应的桶中。

桶内排序:对每个桶中的元素进行排序,例如:

buckets[0] = [0.12]
buckets[1] = [0.17]
buckets[2] = [0.21, 0.23, 0.26]  # 原桶内为 [0.26, 0.23, 0.21]
buckets[3] = [0.39, 0.78, 0.68, 0.72]
buckets[4] = [0.94]

合并桶:将所有桶中的元素按顺序合并:

sorted_arr = [0.12, 0.17, 0.21, 0.23, 0.26, 0.39, 0.68, 0.72, 0.78, 0.94]

#以上只是一种运用场景,大家要灵活根据实际场景来改变元素与桶的关系

3、魔术方法

Python 中的魔术方法(Magic Methods),也称为双下划线方法(Dunder Methods),是以双下划线(__)开头和结尾的特殊方法。它们允许对象实现并自定义类的一些特殊行为,例如运算符重载、对象创建、对象销毁等。(其实就是自己定义函数自己用)

这里只介绍部分,有兴趣的朋友可以去网上找找资料~

(1)__init__ (构造函数):__init__ 是初始化方法,当一个类的实例被创建时,会自动调用它来初始化该实例。

class MyClass:
    def __init__(self, value):
        self.value = value

obj = MyClass(10)

#也可以理解为只要函数前面用了这个方法,只要运行无论是否调用它都会执行以此为开头的函数

(2)比较运算符:重载比较运算符,如 <, <=, ==, !=, >, >= 等。

        eg:__eq__ (相等):

class MyClass:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        return self.value == other.value

obj1 = MyClass(10)
obj2 = MyClass(10)
obj3 = MyClass(20)
print(obj1 == obj2)  # 输出: True
print(obj1 == obj3)  # 输出: False

(3)运算符重载:

通过定义特殊的魔术方法,可以重载常见的运算符,如 +, -, *, / 等。

        eg:__add__ (加法):

class MyClass:
    def __init__(self, value):
        self.value = value

    def __add__(self, other):
        return MyClass(self.value + other.value)

obj1 = MyClass(10)
obj2 = MyClass(20)
obj3 = obj1 + obj2
print(obj3.value)  # 输出: 30

由上述例子我们可以看出,魔术方法是 Python 面向对象编程中的一个强大特性,允许我们自定义对象的行为,使其更符合直观的语义。

4、结语

这篇文章还是挺长的,内容不少,需要大家花点时间静下心来想

有不清楚的地方可以打在评论区,我看到会马上回复哒~

写到结尾,想一想,下一篇还是写数学建模相关的编程实现方法吧,机器学习的内容下下篇再来~

好啦,本期文章就到这里,祝大家学习生活愉快!

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值