排序算法
【谁教你这么剪的 | 11大排序的原理讲解和Python源码剖析】 https://www.bilibili.com/video/BV1Zs4y1X7mN/?share_source=copy_web&vd_source=ed4a51d52f6e5c9a2cb7def6fa64ad6a
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
**空间复杂度:**是指算法在计算机内,执行时所需存储空间,它也是数据规模n的函数。
平均时,空复杂度【o(n^2),o(1)】
冒泡 Bubble Sort-o(n^2),o(1)
冒泡排序(Bubble Sort)是一种简单的排序算法。**它通过重复比较相邻元素,并将其中最大(或最小)的元素逐渐调换到正确的位置。**简单来说,就是把小的元素往前调或者把大的元素往后调。
为什么冒泡排序是一种稳定的排序算法?
- 在冒泡排序中,当相邻的两个元素相等时,它们的位置不会交换,保持了它们在原始数组中的相对顺序。因此,冒泡排序是一种稳定的排序算法。
如果两个相等的元素相邻,冒泡排序只会比较元素大小不会交换。如果两个相等的元素没有相邻,即使通过前面的交换使它们相邻,由于它们相等,不会再发生交换,所以它们的顺序保持不变。因此,冒泡排序是一种稳定的排序算法。
#每次比较的元素数量会减少一对,直到没有需要交换的元素,排序完成。
def bubble_sort(lst):
n = len(lst)
for i in range(n-1,-1,-1):#1)从最后一个元素开始,依次比较相邻的两个元素。
for j in range(0, i):
#2)如果顺序错误(比如前一个元素 大于 后一个元素),则交换它们的位置。
if lst[j] > lst[j+1]:
lst[j], lst[j+1] = lst[j+1], lst[j]
return lst
选择 Selection Sort-o(n^2),o(1)
![动画展示](https://i-blog.csdnimg.cn/direct/cb4c07259e324252ae8a4ad220d34c00.png
原理:**每一轮从待排序序列中选出最小的元素,将其放置在已排序序列的末尾。然后,再从剩余的未排序元素中寻找最小元素,放置在已排序序列的末尾。**重复这个过程,直到全部待排序的数据元素个数为零,即完成排序。
特点:
1)每一轮只进行一次交换,因此交换次数相对较少,但是比较次数较多。
2)选择排序是一种不稳定的排序算法:例如,考虑以下序列:[5, 3, 5, 2]。在第一次选择最小元素时,会选择到索引为3的元素2,并将其与索引为0的元素5进行交换,得到序列[2, 3, 5, 5]。由于原序列中的两个5的相对顺序被改变,所以选择排序是不稳定的。
def selection_sort(lst):
for i in range(len(lst) - 1): #遍历列表,i从第一个元素到倒数第二个元素
min_index = i #1)假设当前索引为i的元素为最小值
for j in range(i + 1, len(lst)): #2)遍历未排序部分的元素
if lst[j] < lst[min_index]: #3)如果找到比当前最小值更小的元素
min_index = j #4)更新最小值的索引
lst[i], lst[min_index]=lst[min_index],lst[i]#4)最后,将当前最小值与未排序部分的第一个元素交换位置
return lst
插入 Insertion Sort -o(n^2),o(1)
动画展示:https://gitee.com/cjq1311485460/brush-question-notes/raw/master/4d409d3d864794158b9d00ed87970110.gif
它的原理是将待排序的数据 逐个插入到 已排序的数据序列中的适当位置,直到全部插入完毕。插入排序如同打扑克牌一样,每次将后面的牌插到前面已经排好序的牌中。
具体步骤如下:
- 假设第一个元素已经是有序的序列,将其视为已排序序列。
- 从第二个元素开始,将其与已排序序列中的元素逐个比较,找到合适的位置插入。
- 将待插入元素插入到已排序序列中的适当位置,使得插入后的序列仍然有序。
- 重复步骤2和步骤3,直到所有元素都插入到已排序序列中。
特点:
稳定排序,适用于小规模数据或基本有序的数据。
def insertion_sort(lst):
n=len(lst)
for i in range(1,n):
cur_num, pre_index = lst[i], i-1 #获取当前待插入的元素 和 其前一个元素的索引
while pre_index>=0 and cur_num<=lst[pre_index]: #当 当前待插入的元素 不大于 前一个元素时,就进行插入操作
lst[pre_index + 1] = lst[pre_index] #1)将前一个元素 后移一位
pre_index -= 1 #2)还原前一个元素的索引
lst[pre_index + 1] = cur_num #3)将 当前待插入的元素 插入到正确的位置
return lst
o(nlogn)
希尔排序 Shell Sort o(nlogn),o(1)
动画展示:https://gitee.com/cjq1311485460/brush-question-notes/raw/master/4f3d6ca502bddeb2b466cd2c2054be55.gif
希尔排序(Shell Sort)是插入排序的一种改进算法,通过使用不同的间隔序列来提高排序的效率。
希尔排序的关键在于选择合适的间隔序列,不同的间隔序列可能会影响排序的效率。常用的间隔序列有**希尔增量序列(gap = gap // 2)**和Hibbard增量序列(gap = 2^k - 1),可以根据具体情况选择合适的间隔序列。
具体步骤如下:
- 首先,确定初始的间隔(gap)值,可以将数据序列的长度除以2取整作为初始间隔值。
- 然后,使用间隔值 对 数据序列进行分组,每个分组包含 相隔间隔值个元素。
- 将每个分组内的元素 按照插入排序的方式进行排序。(粗略排序)
- 缩小间隔值,将当前间隔值除以2取整,得到新间隔值。(逐渐精排)
- 重复步骤2到步骤4,直到最后一个间隔值为1。
- 最后,使用间隔值为1进行一次插入排序,完成最终的排序。
特点:
不稳定的排序
def shell_sort(lst):
n = len(lst)
gap = n // 2 # 初始间隔值为列表长度的一半
while gap > 0: #当间隔值大于0时,进行排序
for i in range(gap, n): #遍历列表,从间隔值开始 到 最后一个元素
cur_num=lst[i]
#以间隔值为步长进行插入排序,如果当前元素小于前一个间隔的元素,则交换元素
j=i
while j>=gap and cur_num<lst[j - gap]:
lst[j], lst[j - gap] = lst[j - gap], lst[j]
gap //= 2 # 缩小间隔值
return lst
【重要】归并 Merge Sort-o(nlogn),o(n)
动画展示:“https://gitee.com/cjq1311485460/brush-question-notes/raw/master/20181120110141.gif” alt=“img” style="
该算法使用分治法的思想,将一个未排序的序列 分割成 多个已排序的子序列,然后递归地将这些子序列合并成一个有序的序列。
具体步骤如下:
- 首先,将待排序的序列进行拆分,将序列分割成多个子序列,直到每个子序列只包含一个元素。
- 然后,递归地将这些子序列进行合并。在合并的过程中,不断比较两个子序列的元素,不断将较小的元素放入新的序列中,并将相应子序列的指针向后移动一位,重复这个过程直到其中一个子序列为空。然后将另一个非空的子序列中的剩余元素直接放入新的序列中。
- 重复步骤2和步骤3,直到所有的子序列都合并成一个有序的序列。
特点:
稳定的排序
# 定义合并函数:先两个两个排序归并,再四个四个排序归并,,,
def merge(nums, left, mid, right):
tmp = [] # 合并后的结果序列
i = left # 左子序列的指针
j = mid + 1 # 右子序列的指针
while i <= mid and j <= right:
if nums[i] <= nums[j]: # 1.1)如果左子序列的元素小于等于右子序列的元素
tmp.append(nums[i]) # 1.2)将左子序列的元素添加到结果序列中
i += 1 # 1.3)左子序列指针向后移动一位
else:
tmp.append(nums[j]) # 1.4)否则,将右子序列的元素添加到结果序列中
j += 1 # 1.5)右子序列指针向后移动一位
# 2)将剩余的元素直接添加到结果序列中
tmp.extend(nums[i:mid+1])
tmp.extend(nums[j:right+1])
#原地操作
for i in range(left,right+1):
nums[i]=tmp[i-left]
return tmp
def merge_sort(nums, left, right):
if left == right: return
mid = left + (right - left) // 2
merge_sort(nums, left, mid) # 对左子序列进行递归排序
merge_sort(nums, mid + 1, right) # 对右子序列进行递归排序
return merge(nums, left, mid, right) # 合并左右子序列并返回结果序列
# nums=[1, 2, 2, 3, 5]
nums=[10,10, 9, 9, 8, 7,5,6,4,3,4,2]
print(merge_sort(nums,0,len(nums)-1))
print(nums[len(nums)-3])
【重要】快速 quick sort-o(nlogn),o(logn)
动画展示:<img src"https://gitee.com/cjq1311485460/brush-question-notes/raw/master/9a08519db9e53d08dc353299cfbe2147.gif" alt=“快速排序” style=“zoom:50%;” />
-
为什么快速排序的最坏情况时间复杂度为O(n^2)?
- 在最坏的情况下,选取的基准值是数组中的最大或最小元素,导致分区过程每次只能分出一个元素,这使得递归调用次数达到最大,时间复杂度为O(n^2)。
-
为什么快速排序的平均时间复杂度为O(nlogn)?
- 在平均情况下,基准值能够将数组均匀分割成两部分,使得递归调用的深度为O(logn),每层的分区操作需要O(n)时间,因此整体的时间复杂度为O(nlogn)。
第一种、快排递归流程:
(1) 从数列中挑出一个基准值。
(2) 每次,将所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以放到任一边)。【在这个分区退出之后,该基准就处于数列的中间位置】
(3) 最后,递归地 把"基准值前面的子数列" 和 “基准值后面的子数列” 进行排序。
第二种、快排非递归流程:
通过使用栈来模拟递归调用过程,可以将快速排序算法转化为循环实现,避免了递归调用的额外开销。这种方法可以提高快速排序的效率。
步骤如下:
-
首先,将数组的起始位置 和 终止位置入栈。
-
然后,以栈是否为空为条件,开始进行循环。每次循环时,从栈中取出两个元素作为当前的区间,进行一次快速排序调用。
-
快速排序函数会返回一个分界点。根据这个分界点,将左侧的起点 和 终点、右侧的起点 和终点 进行判断。
-
如果左侧的起点小于终点,将左侧的起点 和 终点入栈,以便后续对该区间进行排序。
-
如果右侧的起点小于终点,将右侧的起点 和 终点入栈,以便后续对该区间进行排序。
-
继续循环,直到栈为空为止。
#[1]递归实现快速排序
# 划分函数,将列表划分为左右两个子序列,并返回划分点的位置
import random
def partition(nums, left, right):
#优化点:将pivot的位置随机化
# randpivot=random.randint(left,right)
# nums[left],nums[randpivot]=nums[randpivot],nums[left]
pivot=left # 再选择最左边的元素作为划分点的值
#将区间内比nums[pivot]小的元素 都排在前面:
#通过交换nums[i],nums[j]实现
j = left+1
for i in range(left+1,right+1):
if nums[i]<=nums[pivot]:
nums[i],nums[j]=nums[j],nums[i]
j+=1
nums[pivot],nums[j-1]=nums[j-1],nums[pivot] #将间隔nums[pivot]放中间位置,从而实现左小右大
pivot=j-1
return pivot
# 【1】递归实现快速排序
def QuickSort(nums: list, left: int, right: int) -> list:
if left >= right: return
p = partition(nums, left, right) # 获取划分点的位置
QuickSort(nums, left, p - 1) # 对左子序列进行递归排序
QuickSort(nums, p + 1, right) # 对右子序列进行递归排序
return nums
# nums=[1, 2, 2, 3, 5]
nums=[10,10, 9, 9, 8, 7,5,6,4,3,4,2]
print(QuickSort(nums,0,len(nums)-1))
print(nums[len(nums)-3])
# 【2】非递归实现快速排序(使用栈)
def QuickSort_No_Stack(nums: list, left: int, right: int) -> list:
temp = [left, right] # 使用栈来保存 待处理的子序列的左右边界
while temp:
j = temp.pop() # 取出栈顶的右边界
i = temp.pop() # 取出栈顶的左边界
index = partition(nums, i, j) # 获取分界点的位置
if i < index - 1: # 如果左子序列存在元素,压入栈中
temp.append(i)
temp.append(index - 1)
if j > index + 1: # 如果右子序列存在元素,压入栈中
temp.append(index + 1)
temp.append(j)
return nums
(一)快速排序的最好情况,时间复杂度O(nlogn),空间复杂度O(logn)
在理想的情况下,选取的分界点刚好就是这个区间的中位数。此时,就和归并排序基本一致了。
过程:
递归的第一层,n个数被划分为2个子区间,每个子区间的数字个数为n/2;
递归的第二层,n个数被划分为4个子区间,每个子区间的数字个数为n/4;
递归的第三层,n个数被划分为8个子区间,每个子区间的数字个数为n/8;
…
递归的第logn层,n个数被划分为n个子区间,每个子区间的数字个数为1;
与归并排序的区别:
归并排序是从最后一层开始,进行merge操作,自底向上;而快速排序则从第一层开始,交换区间中 数字的位置,是自顶向下的。
与归并排序的相似点:
merge操作和快速排序的时间复杂度是一样的。对于每一个区间,交换处理的时候,都需要遍历区间中的每一个元素,每一层的时间复杂度都是O(n)。在理想的情况下,有logn层,所以快速排序最好的时间复杂度为O(nlogn)。
快速排序的最坏情况,时间复杂度O(n^2),空间复杂度O(n)
在最坏的情况下,选取的分界点刚好就是这个区间的最大值或者最小值。
过程:比如,需要对n个数排序,每一次进行交换处理的时候,选取的分界点刚好都是区间的最小值。这样的话,每次操作,都只能将最小值放到第一个位置,而剩下的元素,则没有任何变化。
**结果:**这种方式,对于n个数来说,就需要操作n次,而每一次操作都需要遍历剩下的所有元素,完成交换,所以总时间复杂度为O(n^2)。
堆排序 Heap sort-o(nlogn),o(1)
【堆排序(heapsort)】 https://www.bilibili.com/video/BV1Eb41147dK/?share_source=copy_web&vd_source=ed4a51d52f6e5c9a2cb7def6fa64ad6a
堆排序的核心思想是通过建立一个初始堆来表示无序区,然后不断地将堆顶元素(即最值)输出,并将其与无序区的最后一个元素交换。这样,每次交换后,有序区的长度增加1,无序区的长度减少1,直到无序区为空,排序完成。
为什么堆排序的时间复杂度为O(NlogN)?
- 堆排序首先构建一个堆,这个过程的时间复杂度为O(N)。然后,通过不断调整堆结构来排序,调整每个元素的时间复杂度为O(logN),因此总体时间复杂度为O(NlogN)。
#【写法1】调整堆,使得以i为根节点的子树满足堆的性质
def adjust_heap(lst, i, size): # size:总节点数
if i >= size: return
left_index = 2 * i + 1 # 左子节点的索引
right_index = 2 * i + 2 # 右子节点的索引
largest_index = i # 假设最大值的索引为根节点索引
# 1)如果左子节点大于根节点,更新最大值的索引 为 左子节点的索引
if left_index < size and lst[left_index] > lst[largest_index]:
largest_index = left_index
# 2)如果右子节点大于最大值,更新最大值的索引 为 右子节点的索引
if right_index < size and lst[right_index] > lst[largest_index]:
largest_index = right_index
# 3)如果最大值的索引 不等于 根节点的索引,交换根节点和最大值
# 4),并递归调整交换后的子树
if largest_index != i:
lst[largest_index], lst[i] = lst[i], lst[largest_index]
adjust_heap(lst, largest_index, size)
# 【推荐:写法2】调整堆,使得以i为根节点的子树满足堆的性质
def adjust_heap(lst, i, size): # size:总节点数
dad = i
son = dad * 2 + 1
while son < size: # 若子节点指标在范围内才做比较
if son + 1 < size and lst[son] < lst[son + 1]: # 先比较两个子节点大小,选择最大的
son+=1 # 含递归过程中son的改变
if lst[dad] > lst[son]: # 如果父节点大于子节点代表调整完毕,直接跳出函数
return
else: # 否则交换父子内容再继续子节点和孙节点比较
lst[dad], lst[son] = lst[son], lst[dad]
dad = son
son = dad * 2 + 1
def HeapSort(lst):
n = len(lst)
# 1)初始化大顶堆:完全二叉树-上到下,左到右构建;父节点 一定大于 根节点
# 实现: 建立初始堆,从最后一个 非叶子节点开始,依次向上调整 每个子树 使其满足堆的性质
for i in range(n // 2, -1, -1): # n//2:以一半的子树 作为堆化的根节点,再通过递归,就可构建出整个大顶堆
adjust_heap(lst, i, n)
# 2)在已构建大顶堆的基础上,做排序
for i in range(n - 1, -1, -1): # 从最后一个元素开始,依次将最大值 放到 末尾
lst[0], lst[i] = lst[i], lst[0] # 将根节点(即最大值)与 最后一个元素交换
adjust_heap(lst, 0, i) # 2)调整交换后的堆【即依次“除去”最后一个最大元素的堆】,从而堆顶依次为最大值
return lst
# nums=[1, 2, 2, 3, 5]
nums=[10,10, 9, 9, 8, 7,5,6,4,3,4,2]
print(HeapSort(nums))
print(nums[len(nums)-3])
nums=[10,10, 9, 9, 8, 7,5,6,4,3,4,2]
print(HeapSort(nums))
【o(n+k)】
桶排序 bucket sort-o(k*o(选择的排序算法)),o(k),k为桶
桶排序的思想是将待排序列表划分为多个桶,然后对每个桶中的元素进行选择排序【不固定】,最后将排序后的元素按照桶的顺序依次放回原列表。桶排序适用于待排序列表中元素的分布比较均匀的情况。桶排序的时间复杂度取决于桶的数量和每个桶中元素的排序算法。在这段代码中,使用了冒泡排序对每个桶中的元素进行排序。
为什么桶排序对数据分布均匀的数据效果更好?
- 桶排序将数据分到多个桶中,然后对每个桶内的数据进行选择排序。如果数据分布均匀,每个桶内的数据量相对均衡,排序效率较高。但如果数据分布不均匀,有些桶内可能会有大量数据,导致排序效率下降。
具体的算法步骤如下:
-
划分桶:确定桶的数量和范围。
-
数据入桶:将待排序的数据按照规则放入对应的桶中。
-
桶内排序:对每个桶中的数据进行排序,可以使用其他排序算法,如选择排序、插入排序、快速排序等。
-
数据合并:将每个桶中的数据按照顺序依次合并,得到最终的有序序列。
def selection_sort(lst):
for i in range(len(lst) - 1): #遍历列表,i从第一个元素到倒数第二个元素
min_index = i #1)假设当前索引为i的元素为最小值
for j in range(i + 1, len(lst)): #2)遍历未排序部分的元素
if lst[j] < lst[min_index]: #3)如果找到比当前最小值更小的元素
min_index = j #4)更新最小值的索引
lst[i], lst[min_index]=lst[min_index],lst[i]#4)最后,将当前最小值与未排序部分的第一个元素交换位置
return lst
def bucket_sort(lst):
maxVal, minVal = max(lst), min(lst)
bucketSize = 3 #自定义的值
# 1)计算桶的数量=(待排序列表中的最大值和最小值 的差)// 桶的个数
bucketCount = (maxVal - minVal) // bucketSize + 1
# 2)创建桶
buckets = [[] for i in range(bucketCount)] #[[]*bucketCount]:这些空列表是同一个对象的多个引用。修改其中一个空列表会影响到所有引用了这个空列表的地方
# 3)将元素放入对应的桶中
for num in lst:
buckets[(num - minVal) // bucketSize].append(num)
# 4)对每个桶中的元素进行排序(这里使用了选择排序)
for bucket in buckets:
selection_sort(bucket)
# 5)将排序后的元素放回原列表
lst.clear()# 清空原列表
for bucket in buckets:
lst.extend(bucket)
return lst
计数排序 Counting Sort -o(n+k),o(k),k 是列表中最大值
计数排序的思想是通过统计每个元素出现的次数,然后根据元素的大小顺序将其放回原列表。它适用于待排序列表中元素的范围较小且已知的情况。计数排序的时间复杂度为O(n+k),其中n为待排序列表的长度,k为元素的范围。计数排序是一种稳定的排序算法,因为相同元素的相对顺序不会改变。
为什么计数排序适用于范围较小且已知的数据?
- 计数排序需要创建一个计数数组,其大小等于待排序数据的范围。如果数据范围太大,计数数组会占用大量空间,因此计数排序适用于范围较小且已知的数据。
算法的步骤如下:
Counting Sort 算法步骤总结:
1.创建计数数组:
2.计算最大元素值 max(lst) 并创建一个长度为 max(lst) + 1 的计数数组 cnt,所有初始值为 0。
3.填充计数数组:
4.遍历待排序列表 lst,将每个元素的计数增加,即 cnt[val] += 1,记录每个元素出现的频率。
5.按顺序填充原数组:
6.遍历计数数组 cnt,根据每个值的计数,将元素按顺序放回原数组 lst,直到计数为 0。
结果:排序后的列表 lst 将按非递减顺序排列。
def counting_sort(lst):
n = len(lst)
cntlen = max(lst) + 1 # 1)根据最大元素值创建 计数数组
cnt = [0] * cntlen
# 2)遍历待排序列表,将每个元素放入对应的计数位置中
for val in lst:
cnt[val] += 1
# 3)从计数数组中依次取出元素,按照顺序放回结果数组(即原数组)
i = 0
for val in range(cntlen):
while cnt[val] > 0:
lst[i] = val
cnt[val] -= 1
i += 1
return lst
基数排序 radix sort-o(nd),o(k),d为最大元素的最大位数,k为桶大小
动画展示:<img src"https://gitee.com/cjq1311485460/brush-question-notes/raw/master/9a3483c2b6dec0045db27fcccb7db4dd.gif" alt=“在这里插入图片描述” style=“zoom:50%;” />
基数排序是一种非比较排序算法,它根据数字的每个位上的值进行排序。
为什么基数排序适合对数字进行排序?
- 基数排序通过逐位排序(从最低位到最高位或从最高位到最低位)来处理每个数字的每一位,这使得基数排序特别适合对数字进行排序,尤其是位数较多但每位数的范围较小的情况。
基数排序的过程:
1.确定最大值:找出列表中的最大值以确定排序的位数。
2.按位排序:
3.从个位开始,逐位向左处理每个数字。
4.使用桶对当前位的数字进行分组。
5.按桶的顺序将元素重新组合回原列表。
6.重复:对每一位重复上述过程,直到处理完所有位数。
def radix_sort(lst):
base = 1 # 初始化基数为1,表示当前处理个位数字
maxv = max(lst) # 获取列表中的最大值
while base <= maxv: # 处理所有位数,直到最大值的位数
buckets = [[] for idx in range(10)] # 创建10个桶,用于存储每一位上的数字(0到9)
for num in lst: # 遍历列表中的每个数字
idx = (num // base) % 10 # 计算当前数字的位数上的值
buckets[idx].append(num) # 将数字放入相应的桶中
l = 0 # 重新填充列表的索引
for idx in range(10): # 遍历所有桶
for val in buckets[idx]: # 遍历每个桶中的元素
lst[l] = val # 将桶中的元素放回列表
l += 1 # 移动到列表中的下一个位置
base *= 10 # 将基数乘以10,处理下一个位数
return lst # 返回排序后的列表