hello大家好!这两天忙着做前端去了,没怎么更新博客,正好今天去趟医院,就接着往下写一篇,机器学习的内容我下一篇文章应该会更新~
这篇文章大概讲两种“不一样的算法”,桶排序和堆排序,可能确实不太好理解,会比前面的稍微复杂一点,看不懂的朋友可以去哔站搜搜教程或者多看遍(我相信我应该还是可以讲明白的哈哈哈)然后就是“魔术方法”(Python里面好像是这么叫的啊,感觉还蛮贴切的,java里面叫比较器,c++里面叫函数重载)
好啦,咱们话不多说,直奔主题!开卷了同志们~
1、堆排序
堆排序是一种基于堆数据结构的比较排序算法。堆是一棵完全二叉树,分为最大堆和最小堆(大根堆和小根堆)。最大堆的每个节点的值都大于或等于其子节点的值,最小堆的每个节点的值都小于或等于其子节点的值。堆排序一般采用最大堆。(优先级队列结构就是堆结构,堆顶就是优先级最大的地方)
#堆数据结构是基于完全二叉树的一种特殊树形结构,但并不是所有的完全二叉树都是堆。堆需要满足以下两个条件:
(1)完全二叉树:堆必须是完全二叉树,即除了最后一层外,每一层的所有节点都有两个子 节点,最后一层的节点尽可能集中在左边。
(2)是最大堆(在最大堆中,父节点的值总是大于或等于其子节点的值)或者最小堆(在最 小堆中,父节点的值总是小于或等于其子节点的值)。
我们画图来看看完全二叉树:
#图中只有打叉的不是完全二叉树
再来看看大根堆的示例:
10
/ \
9 8
/ \ / \
7 6 5 4
# 满足父节点大于其两个子节点
小根堆就与其相反:
1
/ \
2 3
/ \ / \
4 5 6 7
# 满足父节点小于子节点
那么,如何将实现堆结构,进行堆排序呢?
堆排序主要包括两个步骤:
- 构建最大堆:将无序数组构建为最大堆。
- 堆排序:将最大堆的根节点(最大值)与最后一个节点交换,然后对前面的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地址进行排序,这时就可以使用基数排序。
回到正题,我们接着介绍桶排序
桶排序的步骤
- 创建桶:创建一个定量的数组(或列表)作为桶。
- 分配元素:遍历输入数据,将每个元素放入对应的桶中。
- 桶内排序:对每个桶中的元素进行排序(可以使用任何排序算法,如插入排序、快速排序等)。
- 合并桶:将所有桶中的元素按顺序合并,得到排序后的数组。
下面是代码展示:
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.12
,max_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、结语
这篇文章还是挺长的,内容不少,需要大家花点时间静下心来想
有不清楚的地方可以打在评论区,我看到会马上回复哒~
写到结尾,想一想,下一篇还是写数学建模相关的编程实现方法吧,机器学习的内容下下篇再来~
好啦,本期文章就到这里,祝大家学习生活愉快!