hello大家好!这期文章的内容还是数据结构与算法这块的~
因为最近在做一个NLP相关的处理模型,论文看得眼花缭乱的,所以就不是很想再更新机器学习的内容了(QAQ)可能下一篇会是吧...
大家看到本期文章的开头,可能会觉得本期文章的内容很杂,其实不然。这里解释一下,稳定性是对之前所有的排序算法的一个总结,而后面的哈希表与有序表是相互关联的,这是新的内容,大家看了就会知道啦~
排序的算法介绍目前就告一段落啦,十大排序算法中比较重要的常用的我都有提到,当然日后如果还有比较有意思的算法我也会更新到博客,分享给大家~
好了,咱们话不多说,直接进入主题
1、排序算法的稳定性及其汇总
排序算法的稳定性指的是在排序过程中,如果两个元素的键值相等,排序前后它们的相对顺序保持不变,那么该排序算法就是稳定的。反之就是没有稳定性。
所谓“两个元素的键值相等”,指的是在排序过程中,元素依据的关键属性或字段(即“键”)的值是相同的。例如,在对一组学生的成绩进行排序时,如果两个学生的成绩一样,这两个学生的成绩就被称为“键值相等”。
举个例子,假设我们要根据年龄对一组人排序:
- 人A:姓名 = 张三,年龄 = 25
- 人B:姓名 = 李四,年龄 = 25
- 人C:姓名 = 王五,年龄 = 30
在这个例子中,张三和李四的年龄相同,因此它们的键值相等。
在排序时,如果算法是稳定的,那么排序前张三在李四之前,排序后张三和李四仍然会保持这种顺序。如果算法是不稳定的,排序后它们的相对顺序可能会改变。
再举一个选择排序的例子
#可以看到,由于选择排序的特性,1要和0位置上面的一号3交换位置,1号3变成了3号3,改变了三之间的已有的顺序,所以选择排序不具有稳定性。
其他算法的是否具有稳定性,这里就不再一一证明了,感兴趣的朋友可以自己去查查资料。下面就我直接给出一个总结式的表格,供大家参考记忆
时间复杂度 | 空间复杂度 | 是否有稳定性 | |
选择 | O(N^2) | O(1) | 否 |
冒泡 | O(N^2) | O(1) | 是 |
插入 | O(N^2) | O(1) | 是 |
归并 | O(N*logN) | O(N) | 是 |
快排 | O(N*logN) | O(logN) | 否 |
堆排 | O(N*logN) | O(1) | 否 |
#这里的快排特指随机选取用来比较的数的版本,其余的不算
#虽然从指标上来说,归并、快排、堆排是一样的,但从常数时间来讲,实验证明,快排是最快的,所以大家平常在使用的时候,如果没有特别的要求空间复杂的,能用快排就用
#目前还没有发现一种排序算法,时间复杂度可以低于O(N*logN),也没发现有算法可以实现在时间复杂度为O(N*logN)的前提下,空间复杂度可以低于O(N)且保持稳定性
!再给大家说一说,一些常见的坑!
归并排序可以做到空间复杂度为O(1),但是很难,需要用到"归并排序内部缓存法",而且改完之后会丢失稳定性。但其实你费了老大劲改,还不如直接用堆排省事...
"原地归并排序"都是没用的东西,他会让其时间复杂度变成O(N^2),还不如用插入排序...
快排可以做到有稳定性,但是很难,可以去看论文"01 stable sort",但是改完后其空间复杂度会变成O(N),还不如用归并省事...
最后,给大家介绍一个综合排序,算是对排序系列的总结啦
**综合排序(Hybrid Sort)**的思想是结合了简单排序算法和更高效的排序算法,以发挥它们各自的优势。例如,当处理小规模数据集时,插入排序的效率很高;而对于大规模数据集,归并排序这样的分治算法则更为高效。常见的综合排序有 Timsort,它结合了插入排序和归并排序的思想。
以下是代码示例
def insertion_sort(arr, left, right):
"""
对数组 arr 的 [left, right] 区间进行插入排序
"""
for i in range(left + 1, right + 1):
key = arr[i]
j = i - 1
# 将当前元素插入到已排序部分的正确位置
while j >= left and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
def merge(arr, left, mid, right):
"""
合并两个已排序的子数组
第一个子数组是 arr[left:mid+1]
第二个子数组是 arr[mid+1:right+1]
"""
n1 = mid - left + 1
n2 = right - mid
# 创建临时数组保存子数组
L = arr[left:mid + 1]
R = arr[mid + 1:right + 1]
i = 0
j = 0
k = left
# 合并两个子数组到原数组
while i < n1 and j < n2:
if L[i] <= R[j]:
arr[k] = L[i]
i += 1
else:
arr[k] = R[j]
j += 1
k += 1
# 处理剩余元素
while i < n1:
arr[k] = L[i]
i += 1
k += 1
while j < n2:
arr[k] = R[j]
j += 1
k += 1
def hybrid_sort(arr, left, right, threshold=32):
"""
综合排序算法(插入排序 + 归并排序)
如果子数组的大小小于阈值 threshold,使用插入排序
否则使用归并排序
"""
if left < right:
if right - left + 1 <= threshold:
# 当子数组较小时,使用插入排序
insertion_sort(arr, left, right)
else:
# 使用归并排序
mid = (left + right) // 2
hybrid_sort(arr, left, mid, threshold)
hybrid_sort(arr, mid + 1, right, threshold)
merge(arr, left, mid, right)
#这里不再对每一种算法详细注释,有不懂的朋友可以去看一下我其他的文章
#下面对代码做一个简单的解析:
-
插入排序(
insertion_sort
):- 适用于小规模数组的排序,通过将每个元素插入到已排序部分的正确位置来完成排序。
- 时间复杂度为 O(N2)O(N^2)O(N2),但对小数组非常有效。
-
归并操作(
merge
):- 归并两个已排序的子数组。将较小的元素依次放入原数组的相应位置,保证排序后的数组是有序的。
- 归并操作的时间复杂度为 O(N)O(N)O(N),其中 NNN 是两个子数组的元素总数。
-
综合排序(
hybrid_sort
):- 结合了插入排序和归并排序。通过设置一个阈值
threshold
,决定是使用插入排序还是归并排序。 - 当子数组的大小小于或等于
threshold
时,使用插入排序;否则使用归并排序。 - 这种组合利用了插入排序在小数组中的高效性和归并排序在大数组中的稳定性与效率。
- 结合了插入排序和归并排序。通过设置一个阈值
2、哈希表及有序表
哈希表(Hash Table)是一种高效的数据结构,用于存储和检索数据。它的核心思想是将数据通过哈希函数映射到数组的一个位置,从而实现快速的数据访问。哈希表能够在平均情况下提供常数时间复杂度(O(1))的查找、插入和删除操作。
-
哈希函数(Hash Function):
- 哈希函数是哈希表的核心部分。它的作用是将键(key)转换为一个数组索引。一个好的哈希函数应该尽可能地将键均匀地分布到数组中,以减少冲突的发生。
- 计算方式一般为
hash(key) % size
,其中hash(key)
是将键转换为整数的哈希值,size
是哈希表的大小。通过对哈希值取模,得到数组的索引位置。
-
哈希表(Hash Table):
- 哈希表是一个固定大小的数组,每个数组位置称为“桶”(bucket)。每个桶可以存储一个或多个键值对。
- 在简单的实现中,哈希表中的每个位置通常为空或包含一个链表,用于存储具有相同哈希值的键值对。
#原理方面我在后续文章还会提到,这里主要讲的还是运用和一些概念
如果只有键(key),没有伴随的数据(value),那就是(set
)这种数据结构
如果两者都有,那就是map这种数据结构
有无伴随数据是他们的唯一区别,底层结构都是一回事
放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小(会在哈希表里面拷贝一份);如果不是基础类型,则按引用传递,内存占用是这个东西内存地址的大小(一律8字节)
所谓"基础类型"就是编程语言中最简单、最基本的数据类型。这些类型通常是语言的核心构建块,用于表示最基本的数据值。基础类型的操作一般具有固定的时间和空间复杂度,且它们通常不可进一步拆分成更小的数据单位。
比如整数,浮点数,字符,布尔值,字符串等
复合类型如数组、列表、字典等
下面我来演示一下map
(在 Python 中通常表示为字典 dict
)和 set
的使用方法
# 创建一个空字典
my_dict = {}
# 创建一个包含初始键值对的字典
my_dict = {'name': 'Alice', 'age': 30, 'city': 'New York'}
# 插入新键值对
my_dict['email'] = 'alice@example.com'
# 更新已有键的值
my_dict['age'] = 31
# 根据键访问值
name = my_dict['name'] # 'Alice'
# 使用 get() 方法访问值,避免键不存在时引发异常
age = my_dict.get('age', 'Unknown') # 31
# 删除键值对
del my_dict['city']
# 使用 pop() 方法删除键值对并返回值
email = my_dict.pop('email', 'No email') # 'alice@example.com'
# 遍历所有键值对
for key, value in my_dict.items():
print(f"{key}: {value}")
# 遍历所有键
for key in my_dict.keys():
print(key)
# 遍历所有值
for value in my_dict.values():
print(value)
# 创建一个空集合
my_set = set()
# 创建一个包含初始元素的集合
my_set = {1, 2, 3, 4}
# 添加单个元素
my_set.add(5)
# 添加多个元素
my_set.update([6, 7, 8])
# 删除单个元素
my_set.remove(4) # 如果元素不存在,会引发 KeyError
# 使用 discard() 方法删除元素,如果元素不存在,不会引发异常
my_set.discard(9)
# 随机删除一个元素并返回
element = my_set.pop()
# 清空集合
my_set.clear()
# 检查集合中是否包含某个元素
contains = 3 in my_set # True 或 False
# 集合之间的运算
set1 = {1, 2, 3}
set2 = {3, 4, 5}
# 交集
intersection = set1 & set2 # {3}
# 并集
union = set1 | set2 # {1, 2, 3, 4, 5}
# 差集
difference = set1 - set2 # {1, 2}
# 对称差集
symmetric_difference = set1 ^ set2 # {1, 2, 4, 5}
#总结:
-
dict
(映射):用于存储键值对,键是唯一的,每个键关联一个值。提供高效的查找、插入和删除操作。 -
set
(集合):用于存储唯一的元素,不允许重复元素,适用于需要进行集合运算的场景。
有序表(Sorted Table)是一种数据结构,旨在保持元素的有序状态,以支持高效的查找、插入、删除操作。不同于普通的无序表,有序表在内部始终保持元素的有序性,从而能够快速地执行各种操作。
有序表的特点
-
有序性:
有序表中的元素按照一定的顺序排列,这种顺序可以是升序、降序或其他自定义的顺序。 -
高效操作:
由于元素的有序性,有序表支持高效的查找操作,例如二分查找。这使得查找操作通常具有对数时间复杂度(O(log n))。 -
插入和删除:
插入和删除操作可能需要重新调整元素的顺序,以保持有序状态。这样可能导致这些操作的时间复杂度高于普通的无序表(如链表的插入和删除操作)。
哈希表可以实现的功能,有序表都可以实现,并且有序表还可以保持key之间有序
放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小(会在哈希表里面拷贝一份);如果不是基础类型,必须提供自己定义的比较器(否则系统不知道怎么排序,会报错),按引用传递,内存占用是这个东西内存地址的大小(一律8字节)
AVL树和红黑树是常见有序表实现(平衡二叉搜索树size balance tree)感兴趣的朋友可以去搜一下,这里就不赘述了(二叉树的内容会在链表后的文章出,但是其实我之前也有提到了,可以去看看我前面讲归并排序的文章)
下面以红黑树为例,进行代码展示(使用 sortedcontainers
库的 SortedDict
类进行的操作。)
from sortedcontainers import SortedDict
# 创建一个有序字典(SortedDict),内部实现红黑树
tree = SortedDict()
# 插入键值对
tree['apple'] = 3
tree['banana'] = 2
tree['cherry'] = 5
# 查找键的值
def search(key):
return tree.get(key, '键不存在')
# 删除键值对
def delete(key):
if key in tree:
del tree[key]
# 打印字典内容
def print_tree():
for key, value in tree.items():
print(f"{key}: {value}")
print("当前字典内容:")
print_tree()
print("\n查找键 'banana' 的值:")
print(search('banana'))
print("\n删除键 'banana'。")
delete('banana')
print("更新后的字典内容:")
print_tree()
有序表的基础操作包含:查找、插入、删除、获取最小元素、获取最大元素、查找前驱和后继、范围查询等
以下为代码展示
from bisect import bisect_left, bisect_right, insort
# bisect 模块主要用于处理有序列表,bisect_left 和 bisect_right 用于查找元素在有序列表中的插入位置,insort 用于将元素插入到有序列表中,并自动保持列表的有序性。
class SortedArray:
def __init__(self):
# 初始化一个空的有序数组
self.array = []
def insert(self, value):
"""
插入操作:
将一个新元素插入到有序数组中,并保持数组的有序性。
使用 insort 方法在正确的位置插入元素。
"""
insort(self.array, value)
def search(self, value):
"""
查找操作:
查找一个元素是否存在于有序数组中。
使用 bisect_left 查找元素的索引位置,如果找到元素,则返回 True,否则返回 False。
"""
index = bisect_left(self.array, value)
return index < len(self.array) and self.array[index] == value
def delete(self, value):
"""
删除操作:
从有序数组中删除一个特定的元素,并保持数组的有序性。
使用 bisect_left 查找元素的索引位置,如果元素存在,则将其删除。
"""
index = bisect_left(self.array, value)
if index < len(self.array) and self.array[index] == value:
self.array.pop(index)
def get_minimum(self):
"""
获取最小元素操作:
返回有序数组中的最小元素,即数组的第一个元素。
如果数组为空,返回 None。
"""
return self.array[0] if self.array else None
def get_maximum(self):
"""
获取最大元素操作:
返回有序数组中的最大元素,即数组的最后一个元素。
如果数组为空,返回 None。
"""
return self.array[-1] if self.array else None
def predecessor(self, value):
"""
查找前驱操作:
查找小于给定值的最大元素(前驱)。
使用 bisect_left 查找元素的位置,如果前面有元素,返回前一个元素;否则返回 None。
"""
index = bisect_left(self.array, value)
return self.array[index - 1] if index > 0 else None
def successor(self, value):
"""
查找后继操作:
查找大于给定值的最小元素(后继)。
使用 bisect_right 查找元素的位置,如果后面有元素,返回后一个元素;否则返回 None。
"""
index = bisect_right(self.array, value)
return self.array[index] if index < len(self.array) else None
def range_query(self, low, high):
"""
范围查询操作:
查找位于给定范围 [low, high] 内的所有元素。
使用 bisect_left 和 bisect_right 确定范围的起点和终点,返回该范围内的所有元素。
"""
start = bisect_left(self.array, low)
end = bisect_right(self.array, high)
return self.array[start:end]
def __str__(self):
# 返回数组的字符串表示,便于打印输出
return str(self.array)
# 示例使用
# 创建一个有序数组实例
sorted_array = SortedArray()
# 插入元素
sorted_array.insert(10)
sorted_array.insert(5)
sorted_array.insert(15)
sorted_array.insert(20)
sorted_array.insert(1)
print("当前有序数组内容:")
print(sorted_array) # 打印有序数组
# 查找元素
print("\n查找值 10:")
print(sorted_array.search(10)) # 查找值为 10 的元素是否存在
# 获取最小值
print("\n数组中的最小值:")
print(sorted_array.get_minimum())
# 获取最大值
print("\n数组中的最大值:")
print(sorted_array.get_maximum())
# 查找前驱
print("\n值 15 的前驱:")
print(sorted_array.predecessor(15)) # 查找值 15 的前驱
# 查找后继
print("\n值 15 的后继:")
print(sorted_array.successor(15)) # 查找值 15 的后继
# 删除元素
print("\n删除值 10。")
sorted_array.delete(10)
print("更新后的有序数组内容:")
print(sorted_array) # 打印更新后的有序数组
# 范围查询
print("\n查找范围 [5, 15] 内的元素:")
print(sorted_array.range_query(5, 15)) # 查询范围内的元素
#代码中的注释已经很详细了,大家可以认真看一下~
#有序表的原理在后续文章会提及,这篇文章主要讲应用和注意事项
3、一些小tip
本来想再写点链表的东西的,但是发现好像已经8000字了,有点太多了,再写感觉人要没了
所以链表还是放到下一篇吧QAQ
这期的内容概念和实际操作都不少,有时间的话还是要多练习,不然可能睡一觉起来就忘了一半...
当然内容讲的都不难,比较基础,适合刚刚入门的朋友慢慢看~
有不懂有问题的朋友可以评论区打出,我看到了就会马上回~
好啦本期文章就到这里,祝大家万事顺利,学习快乐~拜拜~~