目录
一、Low B三人组 - O(n²)
1.冒泡排序
思路:
从第一个数开始,列表每两个相邻的数,如果前面比后面大,则交换
一趟排序过后,后面的有序区增加一个数,前面的无序区减少一个数
def bubble_sort_simpe(alist):
for i in range(len(alist) - 1): # 趟数
for j in range(len(alist) - i - 1): # 控制下标
if alist[j] > alist[j + 1]:
alist[j], alist[j + 1] = alist[j + 1], alist[j]
以上是基础实现,但我们仍可以对其进行改进
如图所示,问题如下:
当我们排序一个后面已经排好序的列表时,会出现排序重复进行的情况,此时我们可以定义
exchange = False
在每一趟的过程中来判断列表是否更改
def bubble_sort(alist):
for i in range(len(alist) - 1): #趟数
exchange = False
for j in range(len(alist) - i - 1):
if alist[j] > alist[j + 1]:
alist[j], alist[j + 1] = alist[j + 1], alist[j]
exchange = True
if not exchange: #如果每经过一趟列表没有更改说明已经排好序
return None
2.选择排序
思路:
假设第一个位置的数为最小数,然后从无序区开始遍历了,每次遍历从列表中选一个最小数,随即把最小数与无序区最左边的元素交换,直至列表有序
def select_sort(alist: list):
for i in range(len(alist) - 1): # i控制无序区最左边的元素
min_index = i
for j in range(i + 1, len(alist)): # j控制无序区的遍历,负责找到最小值
if alist[j] < alist[min_index]:
min_index = j
alist[min_index], alist[i] = alist[i], alist[min_index]
3.插入排序
思路:
记录第第一个元素的index和value,然后依次看后面的元素,如果后面的元素大于第一个,则排在它的后面,主要是记录这个元素的位置,依次将排好序的下标右移
def insert_sort(list: list):
for i in range(1, len(list)): # 摸到的牌的下标
tmp = list[i]
j = i - 1 # j指的是手中最右边一张牌的下标
# 当摸到的牌比最右边牌小时,遍历手中的牌,看摸到的牌,一直往左走,如果看到比摸到的牌小的就插入
while list[j] > tmp and j >= 0: #j=-1 指将牌移到最左边
list[j + 1] = list[j]
j -= 1
list[j + 1] = tmp
二、NB三人组 - O(nlogn)
1.快速排序
思路:
- 取一个元素p(第一个元素),使元素p归位(定义partition方法)
- 列表被p分成两部分,左边都比p小,右边都比p大
- 递归完成排序
问题:
- 递归:最大深度问题999,会消耗系统资源
import sys
sys.setrecursionlimit(100000)
#设置递归最大深度为100000- 最坏情况:时间复杂度O(n²)
不找第一个值,随机找一个值,把第一个数跟随机选的那个数交换
def quick_sort(alist, left, right): # left和right的值每次递归时都会更改
"""
框架
"""
if left < right: # 这个区域至少有两个元素的时候递归 否则不
mid = partition(alist, left, right) # partition方法获取已归位的index
quick_sort(alist, left, mid - 1)
quick_sort(alist, mid + 1, right)
def partition(alist, left, right):
"""
- 先拿一个变量把要归位的元素p(最左边)存起来
- 左边有一个空位(p原来的位置),然后right-1开始找,把比p小的放在左边的空位
- 右边现在也有上次元素移出来时的空位,然后再left+1开始找,把比p大的放在右边的空位
- 最后直至left和right重合,说明这个位置是要归位的位置,放入p
"""
tmp = alist[left] # 存最左边的元素p,创建一个空位好让右边的填过来
while left < right: # left=right时结束,找到归位的位置
while alist[right] >= tmp and left < right: # 先从右边找比tmp小的数放入左边
# 如果右边的所有数都比p大,那么right就会一直走,为了让right与left重合时退出循环限制一下
right -= 1 # 往左走
alist[left] = alist[right] # 把从右边找到的比p大的值 or 自己 放在左边的空位
print(alist, "right")
while alist[left] <= tmp and left < right:
left += 1
alist[right] = alist[left]
print(alist, "left")
if left == right: # left和right重合时,放入p
alist[left] = tmp
return left
2.堆排序
思路:
1.建立堆
2.得到堆顶元素为最大元素
3.去掉堆顶,将堆最后一个元素放到堆顶,此时可通过一次调整重新使堆有序
4.堆顶元素为第二大元素
5.重复步骤 3 直至堆变空注意事项:
- 二叉树的顺序存储方式 - 用列表存
- 堆:一种特殊的完全二叉树结构(大根堆、小根堆)
- 父节点
i
和’左’孩子节点的编号下标关系:i -> 2i+1
父节点i
和’右’孩子节点的编号下标关系:i -> 2i+2
孩子节点j
和父节点:j -> (j-1)//2
def sift(alist: list, low, high):
"""
调整
:param low:堆的根节点位置(堆顶)
:param high:堆的最后一个元素的位置(low需要一层层往下看,然后跟high比,如果low>high则越界)
:return:
"""
i = low # 堆的根节点位置(父亲)
j = 2 * i + 1 # j开始指向i的左孩子
tmp = alist[low] # 把堆顶的元素存起来 i的位置为空
while j <= high: # 只要j位置有数(high指向最后一个元素)
if j + 1 <= high and alist[j + 1] > alist[j]: # 如果有右孩子且右孩子比左孩子大
j += 1 # 则j指向右孩子
if alist[j] > tmp: # 如果孩子比堆顶的元素大,那么孩子上去
alist[i] = alist[j]
i = j # 然后要将i和j的位置同时往下移(循环判断直至tmp插入)
j = 2 * i + 1
else: # 堆顶元素tmp更大,就把tmp放到i的位置然后退出循环
alist[i] = tmp
break
alist[i] = tmp # 最后把tmp放到叶子节点上
def heap_sort(alist):
n = len(alist)
# 先建立堆,从孩子找父亲是j->(j-1)//2,现在孩子是最后一个元素len-1,则父亲就是(n-2)//2
for i in range((n-2) // 2, -1, -1):
# i表示建堆的时候调整的部分的根的下标
sift(alist, i, n - 1) # high还是最后一个元素的下标
# 建堆完成
for i in range(n-1, -1, -1):
# i指向当前堆的最后一个元素
alist[0], alist[i] = alist[i], alist[0] # 把堆顶元素与最后一个元素交换位置
sift(alist, 0, i - 1) # 由于最后一个元素已经是最大的了排好序了,所以i-1是新的high
堆的内置模块
import heapq # q->quene 优先队列
import random
alist = list(range(100))
random.shuffle(alist)
print(alist)
heapq.heapify(alist) # 建堆(小根堆)
print(alist)
for i in range(len(alist)):
print(heapq.heappop(alist)) # 往外弹出一个最小的元素
top k问题及实现
问题:
现在有n个数,设计算法得到前k大的数(k<n)解决方法:
- 1.把所有数排序后切片 - O(nlogn)
- 2.排序LowB三人组 - O(kn)
- 3.堆排序思路 - O(nlogk)
ⅰ取列表前k个元素建立一个小根堆,堆顶就是目前第k大的数(即k中最小的数)
ⅱ依次向后遍历原列表,对于列表中的元素,如果大于堆顶,则堆顶更换为该元素,并且对堆进行一次调整,反之忽略
ⅲ遍历列表所有元素后,倒序弹出堆顶
在原来的堆排序上加入top_k
方法即可
def top_k(alist, k):
heap =alist[0:k] # 先进行切片
for i in range((k-2)//2, -1, -1):
sift(heap, i, k-1) # 1.建堆
for i in range(k, len(alist) - 1):
if alist[i] > heap[0]: # 如果大于堆顶
heap[0] = alist[i] # 覆盖
sift(heap, 0, k-1) # 调整
for i in range(k - 1, -1, -1):
# i指向当前堆的最后一个元素
heap[0], heap[i] = heap[i], heap[0] # 把堆顶元素与最后一个元素交换位置
sift(heap, 0, i - 1) # 由于最后一个元素已经是最大的了排好序了,所以i-1是新的high
return heap
3.归并排序
Python的sort方法是基于归并排序的
归并:把一个两段有序的列表合为一个有序列表
- low mid+1两个指针分别指向这两段有序列表的第一个数,比较他们,小的先排列,然后对应的指针后移
- 后来肯定有一个列表先被遍历完,随即另一个列表的剩余元素全部排列
归并排序整体思路:
- 分解:将列表越分越小,直至分成一个元素,因为一个元素是有序的
- 合并:将两个有序列表合并,列表越来越大
# 假设有一个两段有序的列表
def merge(alist, low, mid, high):
i = low # 第一段的第一个
j = mid + 1 # 第一段的第一个
tmp_list = []
while i <= mid and j <= high: # 只要两边都有数
if alist[i] < alist[j]:
tmp_list.append(alist[i])
i += 1
else:
tmp_list.append(alist[j])
j += 1
# 现在有一部分没数了
while i <= mid: # 第一部分有数
tmp_list.append(alist[i])
i += 1
while j <= high: # 第二部分有数
tmp_list.append(alist[j])
j += 1
alist[low:high+1] = tmp_list # 把这个临时列表替换回去
def merge_sort(alist, low, high): # low和high分别指向第一个和最后一个元素下标
if low < high: # 至少有两个,递归分解,后合成
mid = (high + low) // 2
merge_sort(alist, low, mid) # 递归左边
merge_sort(alist, mid+1, high) # 递归右边
merge(alist, low, mid, high) # 合并左边和右边
小结
一、时间复杂度:O(nlogn)
二、运行时间的快慢:快排 > 归并排序 > 堆排序
三、缺点:
- 快排:极端情况下排序效率低
- 归并排序:需要额外的内存开销(不是原地排序)
- 堆排序:在快的排序算法中相对较慢
- 稳定性:有顺序的、挨个换的排序才具有稳定性
【Python的sort不使用快排的原因:不稳定】
稳定:排序后保证两个数的相对顺序不变
例如:
在字典中对name进行排序
{'name':'a', 'age':18}
{'name':'b', 'age':20}
{'name':'a', 'age':25}
排序后:
{'name':'b', 'age':20}
{'name':'a', 'age':18}
{'name':'a', 'age':25}
底下两个name的相对位置不更改
三、其他排序
1.希尔排序
希尔排序是一种分组插入排序算法(基于插入排序)
它的时间复杂度比较复杂,并且和选取的gap序列有关思路:
- 首先取一个整数d₁ = n/2,将元素分为d₁个组,每组相邻量元素之间的距离为d₁,在各族进行插入排序
- 取第二个整数d₂ = d₁/2,重复上述分组排序过程直至dᵢ=1,即所有元素在同一组内进行插入排序
希尔排序每趟并不使某些元素有序,而是使整体数据越来越接近有序,最后一趟排序使得所有元素有序
这里的insert_sort_gap
方法是将插入排序代码中的1 -> gap
def insert_sort_gap(alist, gap): # gap表示组
for i in range(gap, len(alist)):
tmp = alist[i]
j = i - gap
while alist[j] > tmp and j >= 0:
alist[j + gap] = alist[j]
j -= gap
alist[j + gap] = tmp
def shell_sort(alist):
d = len(alist) // 2
while d >= 1:
insert_sort_gap(alist, d)
d //= 2
2.计数排序
问题:
对列表进行排序,已知列表中的数范围都在0~100之间。设计时间复杂度为O(n)的算法解决:创建一个临时列表统计这些数的值和个数,然后放回原列表
def count_sort(li: list, max_count=100):
count = [0 for _ in range(max_count+1)] # 创建一个临时列表
for val in li:
count[val] += 1 # 把对应的值的个数放入count中,下标对应的就是值
li.clear()
for ind, val in enumerate(count): # enumerate 取出列表中对应的下标和值
for i in range(val):
li.append(ind)
3.桶排序
桶排序的时间复杂度取决于数据的分布
平均:O(n+k)
思路:
Bucket sort
:首先将元素分在不同的桶中,在对每个桶中的元素排序
def bucket_sort(li, n=100, max_num=10000): # 把这些元素分到100个桶里,这些元素的最大值是10000
buckets = [[] for _ in range(n)] # 创建一个二维列表表示100个桶
for var in li:
i = min(var // (max_num // n), n-1) # i表示var放到几号桶,当n=10000放到99号桶
buckets[i].append(var) # 把var加到桶里面
for j in range(len(buckets[i])-1, 0, -1): # 保持桶内的顺序(插入排序)
if buckets[i][j] < buckets[i][j-1]:
buckets[i][j], buckets[i][j-1] = buckets[i][j-1], buckets[i][j]
else:
break
sorted_li = []
for buc in buckets:
sorted_li.extend(buc) # extend - 追加列表
return sorted_li
4.基数排序
思路:
radix sort:先按照个位分到0~9号桶,随后按个位依次输出,然后按十位分桶直至所有数全部排好
迭代的次数由最大的数决定
注:9的十位以上的位都看成0
def radix_sort(nums: list[int]):
max_num = max(nums) # 99->2 99迭代2次
iteration = 0 # 迭代的次数
while 10 ** iteration <= max_num:
buckets = [[] for _ in range(10)] # 分10个桶代表0~9
for var in nums: # 分桶 - var表示variable变量
digit = (var // 10 ** iteration) % 10 # it=0取个位...
buckets[digit].append(var)
nums.clear()
for buc in buckets:
nums.extend(buc) # 追加列表,把数重新写回nums
iteration += 1