经典排序算法
参考文章:
1. 冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1.1 算法描述
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
1.2 代码实现
# 时间复杂度:0(n^2)
# 空间复杂度:O(1)
def bubble_sort(data):
length = len(data)
switch = False
for i in range(length):
for j in range(length-i-1):
# 内层循环依次比较相邻的两个元素,一轮结束最大的元素排在末尾
if data[j] > data[j+1]:
data[j], data[j+1] = data[j+1], data[j]
switch = True
# 如果一轮结束没有发生交换说明已经排好序
if not switch:
break
return data
if __name__ == '__main__':
data = [3, 2, 1, 5, 6, 4]
print(bubble_sort(data))
2. 选择排序
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
注意:选择排序相对于冒泡排序只是少了每个相邻元素交换的动作,改为找到最小值下标并交换一次。
2.1 算法描述
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
- 初始状态:无序区为R[1…n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1趟结束,数组有序化了。
2.2 代码实现
# 时间复杂度:O(n^2)
# 空间复杂度:O(1)
def select_sort(data):
length = len(data)
for i in range(length):
# 剩余未排序元素中最小元素的下标
min_index = i
for j in range(i, length):
if data[j] < data[min_index]:
min_index = j
# 经过一轮循环,找到最小元素下标并交换位置
data[min_index], data[i] = data[i], data[min_index]
return data
if __name__ == '__main__':
data = [3, 2, 1, 5, 6, 4]
print(select_sort(data))
3. 插入排序
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
3.1 算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
3.2 代码实现
相对于冒泡和选择排序时间复杂度没有变化,只是算法思想不同。
# 时间复杂度:O(n^2)
# 空间复杂度:O(1)
def insert_sort(data):
length = len(data)
for i in range(1, length):
# 记录当前节点的值
cur = data[i]
j = i
# 倒序遍历,如果前一个数比cur大,则将前一个数后移一位
while j > 0 and cur < data[j-1]:
data[j] = data[j-1]
j -= 1
# 遍历到比cur小的数时退出循环,将cur赋值给当前下标
else:
data[j] = cur
return data
if __name__ == '__main__':
data = [3, 2, 1, 5, 6, 4]
print(insert_sort(data))
4. 希尔排序
希尔排序是插入排序的一种变种。如果数组的最大值刚好是在第一位,使用插入排序要将它挪到正确的位置就需要 n - 1 次移动。也就是说,原数组的一个元素如果距离它正确的位置很远的话,则需要与相邻元素交换很多次才能到达正确的位置,这样是相对比较花时间了。
希尔排序就是为了加快交换速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序。
希尔排序的思想是采用插入排序的方法,先让数组中任意间隔为 h 的元素有序,刚开始 h 的大小可以是 h = n / 2,接着让 h = n / 4,让 h 一直缩小,当 h = 1 时,也就是此时数组中任意间隔为1的元素有序,此时的数组就是有序的了。
4.1 代码实现
def shell_sort(data):
"""
时间复杂度: O(nlogn)
空间复杂度: O(n)
① 设置间隔从len(num_list)//2开始;每次缩小间隔gap //= 2
② 通过gap获取num_list的每个子列表,对子列表采用插入排序
③ 核心部分使用插入排序。当gap为1时就是真正的插入排序
:param data:
:return:
"""
length = len(data)
gap = length // 2
while gap > 0:
for start in range(gap):
insert_sort(data, start, gap)
gap //= 2
return data
# 核心部分代码就是插入排序加上间隔gap,当gap为1时就是普通的插入排序
def insert_sort(data, start, gap):
length = len(data)
for i in range(start+gap, length, gap):
# 记录当前节点的值
cur = data[i]
j = i
# 倒序遍历,如果前一个数比cur大,则将前一个数后移一位
while j > 0 and cur < data[j-1]:
data[j] = data[j-1]
j -= 1
# 遍历到比cur小的数时退出循环,将cur赋值给当前下标
else:
data[j] = cur
return data
if __name__ == '__main__':
data = [3, 2, 1, 5, 6, 4]
print(shell_sort(data))
5. 归并排序
归并排序是分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为归并。
5.1 算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用递归归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
5.2 代码实现
# 时间复杂度: O(nlogn)
# 空间复杂度: O(n) 合并时需要额外的空间保存合并列表
def merge_sort(data):
# 递归结束条件:只有1个元素,即有序的
if len(data) <= 1:
return data
# 拆分为更小的列表,递归调用自身进行拆分、合并
middle = len(data) // 2
left = merge_sort(data[:middle])
right = merge_sort(data[middle:])
# 合并
merge = list()
while left and right:
if left[0] <= right[0]:
merge.append(left.pop(0))
else:
merge.append(right.pop(0))
merge.extend(right if right else left)
return merge
if __name__ == '__main__':
data = [3, 2, 1, 5, 6, 4]
print(merge_sort(data))
6. 快速排序
快速排序的基本思想:从数组中选择一个元素,把这个元素称之为中轴元素,然后把数组中所有小于中轴元素的元素放在其左边,所有大于或等于中轴元素的元素放在其右边,显然,此时中轴元素所处的位置的是有序的。也就是说,无需再移动中轴元素的位置。
从中轴元素那里开始把大的数组切割成两个小的数组(两个数组都不包含中轴元素),接着通过递归的方式,让中轴元素左边的数组和右边的数组也重复同样的操作,直到数组的大小为1,此时每个元素都处于有序的位置。
6.1 算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot)(一般选择第一个元素);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
6.2 代码实现
# 时间复杂度:O(nlogn) 如果总能根据中值把左右两边子列表分成相等的两部分,那么可以达到nlogn;快排的关键是中值选择。
# 空间复杂度:O(1)
def quick_sort(data, start, end):
if start >= end:
return data
# 找到中轴元素下标
pivot = partition(data, start, end)
# 递归调用,中轴元素左右两边的子列表
quick_sort(data, start, pivot-1)
quick_sort(data, pivot+1, end)
return data
def partition(data, start, end):
# 记录中轴元素
middle_val = data[start]
left = start + 1
right = end
while True:
# 向右移动找到第一个比middle_val大的下标
while left <= right and data[left] <= middle_val:
left += 1
# 向左移动找到第一个比middle_val小的下标
while left <= right and data[right] >= middle_val:
right -= 1
if left > right:
break
# 交换两个元素,使得左边的元素不大于middle_val, 右边的不小于middle_val
data[left], data[right] = data[right], data[left]
# 使中轴元素处于有序的位置
data[start], data[right] = data[right], data[start]
return right
if __name__ == '__main__':
data = [3, 2, 1, 5, 6, 4]
print(quick_sort(data, 0, len(data)-1))
7. 堆排序
堆的特点就是堆顶的元素是一个最值,大顶堆的堆顶是最大值,小顶堆则是最小值。
堆排序就是把堆顶的元素与最后一个元素交换,交换之后破坏了堆的特性,再把堆中剩余的元素再次构成一个大顶堆(小顶堆),然后再把堆顶元素与最后第二个元素交换….如此往复下去,等到剩余的元素只有一个的时候,此时的数组就是有序的了。
一般构建堆时:大顶堆—升序、小顶堆—降序。
def heap_sort_asc(lst):
# 构建大顶堆
length = len(lst)
# 从最后一个非叶子节点开始调整,完全二叉树的最后一个非叶子节点是n//2 - 1
i = length // 2 - 1
for j in range(i, -1, -1):
perc_down(lst, j, length-1)
# 大顶堆顶部元素是最大值,所以和最后一个元素交换,最后的元素是最大值---排序后是升序
for i in range(length-1):
lst[0], lst[length-1-i] = lst[length-1-i], lst[0]
perc_down(lst, 0, length-1-1-i)
return lst
def heap_sort_desc(lst):
# 构建小顶堆
length = len(lst)
i = length//2-1
for j in range(i, -1, -1):
perc_down1(lst, j, length-1)
# 堆排序---降序
for i in range(length-1):
lst[0], lst[length-1-i] = lst[length-1-i], lst[0]
perc_down1(lst, 0, length-1-1-i)
return lst
def perc_down(lst, n, length):
temp = lst[n]
child = 2*n + 1
while child <= length:
# 右节点存在并且值大于左节点,选择右节点下沉
if child+1 <= length and lst[child+1] > lst[child]:
child += 1
# 当前节点大于等于子节点时,下沉完毕
if lst[child] <= temp:
break
lst[n] = lst[child]
n = child
child = 2*n + 1
lst[n] = temp
def perc_down1(lst, n, length):
temp = lst[n]
child = 2*n + 1
while child <= length:
# 右节点存在并且值小于左节点,选择右节点下沉
if child+1 <= length and lst[child+1] < lst[child]:
child += 1
# 当前节点小于等于子节点时,下沉完毕
if lst[child] >= temp:
break
lst[n] = lst[child]
n = child
child = 2*n + 1
lst[n] = temp
if __name__ == '__main__':
lst = [6, 5, 1, 2, 3, 4]
# 升序---构建大顶堆---堆顶元素和最后一个元素交换
print(heap_sort_asc(lst))
# 降序---构建小顶堆---堆顶元素和最后一个元素交换
print(heap_sort_desc(lst))
8. 计数排序
计数排序是一种适合于最大值和最小值的范围不是很大的排序。
**基本思想:**把数组元素作为数组的下标,然后用一个临时数组统计该元素出现的次数,例如 temp[i] = m,表示元素 i 出现了 m 次。最后再把临时数组统计的数据从小到大汇总起来,此时汇总起来是数据是有序的。
# 时间复杂度:O(n+k)
# 空间复杂度:O(k),k是桶个数
def bucket_sort(nums):
# 1. 找到最值
min = nums[0]
max = nums[0]
for i in nums:
if i < min:
min = i
if i > max:
max = i
# 2. max-min+1即数组最大范围,将min作为偏移量
bucket = [0]*(max - min + 1)
# 下标代表数字-min,值代表出现的次数
for i in nums:
bucket[i-min] = bucket[i-min] + 1
# 3. 把统计后的数据修改到原数组上
# 记录下标值
k = 0
for index, val in enumerate(bucket):
# val代表出现了几次,出现几次就添加几个
for i in range(val):
# 下标加上min恢复为原数据;
nums[k] = index + min
k += 1
return nums
if __name__ == '__main__':
nums = [5, 3, 1, 1, 1, 3, 9, 1]
print(bucket_sort(nums))
LeetCode
215. 数组中的第K个最大元素
解法1:快速排序
使用快速排序将原列表降序排序后,再找第k个元素。
# 时间复杂度:O(nlogn), 快排理想情况是nlogn,最差是n^2
# 空间复杂度:O(1)
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
length = len(nums)
quick_sort(nums, 0, length-1)
# 降序排序后获取第k-1个元素
return nums[k-1]
def quick_sort(lst, start, end):
if start < end:
# 获取中轴
pivot = partition(lst, start, end)
quick_sort(lst, start, pivot-1)
quick_sort(lst, pivot+1, end)
def partition(lst, start, end):
# 将第一个元素作为中间值
middle_index = start
left = start + 1
right = end
while True:
# 中间值左侧的元素大于中间值
while left <= right and lst[left] >= lst[middle_index]:
left += 1
# 中间值右侧的元素小于中间值
while left <= right and lst[right] <= lst[middle_index]:
right -= 1
if left > right:
break
# 交换不满足条件是值
lst[left], lst[right] = lst[right], lst[left]
# 将中间值交换值中轴
lst[middle_index], lst[right] = lst[right], lst[middle_index]
return right
解法2:堆排序
- 构建大顶堆。
- 进行堆排序;因为要获取第k大的元素,所以删除k-1次堆顶元素后堆顶的元素就是第k大的。
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
length = len(nums)
heap_sort(nums, length, k)
return nums[0]
def heap_sort(lst, length, k):
# 1. 构建大顶堆
# 获取最后一个非叶子节点
i = length//2-1
for j in range(i, -1, -1):
site_down(lst, j, length-1)
# 2. 堆排序---获取第k大的元素,即删除k-1次顶部元素后顶部的元素就是第k大。
for i in range(k-1):
lst[0], lst[length-1-i] = lst[length-1-i], lst[0]
site_down(lst, 0, length-1-1-i)
def site_down(lst, n, length):
temp = lst[n]
# 左子节点
child = 2*n+1
while child <= length:
# 选择左右子节点中更小的节点
if child+1 <= length and lst[child+1] > lst[child]:
child += 1
# 子节点都大于temp时停止下沉
if temp >= lst[child]:
break
lst[n] = lst[child]
n = child
child = 2*n + 1
lst[n] = temp
347. 前 K 个高频元素
解法1:桶排序
# 时间复杂度:O(n)
# 空间复杂度:O(k),存放mapping和bucket的空间
def topKFrequent(nums, k):
# 1.获取i和i个数的映射
mapping = {}
for i in nums:
mapping[i] = mapping.get(i, 0) + 1
# 2. 获取次数和元素映射; 找到出现次数的最小值和最大值;
# 获取最大值和最小值是为了减小建桶空间的占用
min_val = 0
max_val = 0
count_num_map = {}
for key, value in mapping.items():
if value < min_val:
min_val = value
elif value > max_val:
max_val = value
temp = count_num_map.get(value, [])
temp.append(key)
count_num_map[value] = temp
# 3. 建桶,桶下标代表出现出现的次数,桶内放出现的元素
bucket = [None] * (max_val-min_val+1)
for index, num in enumerate(bucket):
if index in count_num_map:
bucket[index] = count_num_map[index]
# 4. 倒序遍历桶获取k个元素
result = []
for i in range(len(bucket)-1, 0, -1):
if len(result) < k and bucket[i]:
result.extend(bucket[i])
return result
if __name__ == '__main__':
nums = [1, 1, 1, 2, 2, 3]
k = 2
print(topKFrequent(nums, k))
解法2:堆排序
- 获取每个元素和元素的个数映射关系: {“key1”: “key1个数”, “key2”: “key2个数”…};
- 根据每个元素的个数构建小顶堆。
def topKFrequent(nums, k):
mapping = {}
# 1. 获取 i 和 i 个数的映射
for i in nums:
mapping[i] = mapping.get(i, 0) + 1
return heap_sort(mapping, k)
def heap_sort(mapping, k):
# 2. 构建小顶堆
result = []
for key, val in mapping.items():
# 如果当前堆的元素个数小于k,则直接加入堆顶并执行下沉
if len(result) < k:
result.insert(0, key)
site_down(result, 0, len(result)-1, mapping)
# 如果当前堆得元素个数等于k,则比较当前key和堆顶元素的个数大小,如果大于堆顶元素个数则替换到堆顶并执行下沉。
else:
if val > mapping[result[0]]:
result[0] = key
site_down(result, 0, len(result)-1, mapping)
return result
def site_down(result, n, length, mapping):
temp = result[n]
# 默认选择左子节点
child = 2*n+1
while child <= length:
# 选择左右子节点中更小的节点进行下沉
if child+1 <= length and mapping[result[child+1]] < mapping[result[child]]:
child += 1
# 左右子节点都大于等于temp时停止下沉
if mapping[temp] <= mapping[result[child]]:
break
# 子节点上升到当前节点
result[n] = result[child]
n = child
child = 2*n + 1
# 最后将当前节点交换到子节点
result[n] = temp
if __name__ == '__main__':
nums = [1, 1, 1, 2, 2, 3]
k = 2
print(topKFrequent(nums, k))
451. 根据字符出现频率排序
解法1:桶排序
def topKFrequent(s):
# 1.获取i和i个数的映射
mapping = {}
for i in s:
mapping[i] = mapping.get(i, 0) + 1
# 2. 获取次数和元素映射; 找到出现次数的最小值和最大值;
min_val = 0
max_val = 0
count_num_map = {}
for key, value in mapping.items():
if value < min_val:
min_val = value
elif value > max_val:
max_val = value
temp = count_num_map.get(value, [])
temp.append(key)
count_num_map[value] = temp
# 3. 建桶,桶下标代表出现出现的次数,桶内放出现的元素
bucket = [[]] * (max_val-min_val+1)
for index, num in enumerate(bucket):
if index in count_num_map:
bucket[index] = count_num_map[index]
# 4. 倒序遍历桶获取k个元素
result = []
for i in range(len(bucket)-1, 0, -1):
for item in bucket[i]:
for j in range(i):
result.extend(item)
return "".join(result)
if __name__ == '__main__':
s = "tree"
print(topKFrequent(s))
75. 颜色分类
解法1
def sortColors(nums):
mapping = {}
for i in nums:
mapping[i] = mapping.get(i, 0) + 1
index = 0
for i in range(3):
if index >= len(nums):
break
for j in range(mapping.get(i, 0)):
nums[index] = i
index += 1
return nums
if __name__ == '__main__':
nums = [2,0,2,1,1,0]
print(sortColors(nums))
解法2:堆排序
直接使用堆排序
# 时间复杂度:O(nlogn)
# 空间复杂度:O(1)
def sortColors(nums):
return heap_sort(nums)
def heap_sort(nums):
# 1. 构建大顶堆
# 最后一个非叶子节点
length = len(nums)
for i in range(length//2-1, -1, -1):
site_down(nums, i, length-1)
# 2. 排序
for i in range(length-1):
nums[0], nums[length-i-1] = nums[length-i-1], nums[0]
site_down(nums, 0, length-i-1-1)
return nums
def site_down(nums, n, length):
temp = nums[n]
# 默认选择左子节点
child = 2*n+1
while child <= length:
# 选择左右子节点中更大的节点进行下沉
if child+1 <= length and nums[child+1] > nums[child]:
child += 1
# 左右子节点都小于等于temp时停止下沉
if temp >= nums[child]:
break
# 子节点上升到当前节点
nums[n] = nums[child]
n = child
child = 2*n + 1
# 最后将当前节点交换到子节点
nums[n] = temp
if __name__ == '__main__':
nums = [2, 0, 2, 1, 1, 0]
print(sortColors(nums))