图片来源:(排序算法总结 - 国内版 Bing images)
- 递归需要系统空间栈,递归走n层,最少是logn层;
- 稳定性:指相同两个数的相对位置(顺序),相对位置变了就是不稳定,没变就是稳定的;
- 总结:挨个元素交换就是稳定的,跳着交换就是不稳定的。
列表排序
● 排序:将一组“无序”的记录序列调整为“有序”的记录序列。
● 列表有序:将无序列表变为有序列表
▶ 输入:列表
▶输出:有序列表
● 升序与降序
● 内置排序函数:sort() ,python 中内置的sort() 的 排序内部实现是 timsort。
详见原文链接:python sort函数内部实现原理 - 焦国峰的随笔日记 - 博客园 (cnblogs.com)
常见排序算法
排序Low B三人组 | 排序NB三人组 | 其他排序 |
冒泡排序 | 快速排序 | 希尔排序 |
选择排序 | 堆排序 | 计数排序 |
插入排序 | 归并排序 | 基数排序 |
Low B三人组:冒泡、选择、插入
(1)冒泡排序(Bubble Sort)
算法思想
● 列表每两个相邻的数,如果前面比后面大,则交换这两个数。
● 一趟排序完成后,则无序区减少一个数,有序区增加一个数。
利用i来表示总共要走多少趟,i-1趟
j表示指针,控制是否要做交换,如果li[j] < li[j+1],则交换;否则不交换;j++
# 冒泡排序要排序n个数(假设为升序排列), 由于每遍历一趟只排好一个数字 即把最大数排到最后 # 则需要遍历n-1趟,所以最外层循环是要循环n-1次 # 每完成一趟遍历后,下趟需要比较的数字就会减少一个 # 所以第i趟需要比较的次数为len(list)-i-1,所以第二层循环要遍历是n-1-i次
动图解释
代码实现
import random
# 复杂度为:O(n^2)
def bubble_sort_simple(li):
"""暴力冒泡排序"""
# 外层循环控制总共需要遍历的次数,需要len(li)-1次
for i in range(len(li)-1): # i表示趟数
# 内层循环控制每一趟需要比较的次数,对于已经排好的数字(大数字)就不需要再比较了,
# 则需要比较len(list)-1-i次
for j in range(len(li)-i-1): # 排好的就不用遍历了
# 如果第j个数大于第j+1个数 则互换位置,在Python中,for循环中的变量会自动+1
if li[j] > li[j+1]:
# 满足条件,即交互位置
li[j], li[j+1] = li[j+1], li[j]
return li
li = [random.randint(0,10) for i in range(10)]
print(bubble_sort_simple(li))
改进后的冒泡排序:
def bubble_sort(li):
"""改进后的冒泡排序"""
for i in range(len(li)-1):
# 创建一个标志位,用来记录本轮冒泡,是否有数据交换位置
status = False
for j in range(len(li)-i-1):
if li[j] > li[j + 1]:
li[j], li[j + 1] = li[j + 1], li[j]
# 只要由数据交换位置,则修改statusd的值
status = True
# 每一轮冒泡结束之后,判断当前status是否为Flase,
# 如果为Flase,则说明上一轮冒泡没有修改任何数据的顺序(即数据是有序的)
if not status:
return li
return li
li = [random.randint(0,10) for i in range(10)]
print(bubble_sort(li))
(2)选择排序(Select Sort)
算法思想
一趟排序记录最小的数,放到第一个位置
再一趟排序记录列表无序区最小的数,放到第二个位置
......
算法关键点:有序区和无序区、无序区最小数的位置
选择排序的主要思想是:先从整个序列中选择最小的数据放到第一位,再从剩余的序列中选择最小的数据放在第二位,如此循环,直到最后一位。
代码参考:https://blog.csdn.net/zhicheng_xu/article/details/93747537
动图解释
代码实现
# 时间复杂度:O(n^2)
def select_sort_simple(li):
"""暴力选择排序"""
result = []
for i in range(len(li)):
min_val = min(li) # O(n)
result.append(min_val) # O(1)
li.remove(min_val) # O(n)
return result
# 时间复杂度:O(n^2)
def select_sort(li):
"""改进后的选择排序"""
for i in range(len(li) - 1):
min_index = i
for j in range(i+1, len(li)):
if li[j] < li[min_index]:
min_index = j
if i != min_index:
li[min_index], li[i] = li[i], li[min_index]
return li
if __name__ == '__main__':
li = [2,3,6,1,0,5,8]
select_sort_simple(li)
select_sort(li)
(3) 插入排序
算法思想
基本思想是每一步将一个待排序的数据插入到前面已经排好序的有序序列中,直到插完所有元素为止。
模拟抓牌的过程:
● 初始时手里(有序区)只有一张牌
● 每次(从无序区)摸一张牌,插入到手里已有牌的正确位置
动图解释
代码实现
# 时间复杂度:O(n^2)
def insert_sort(li):
for i in range(1, len(li)): # i表示摸到的牌的下标
tmp = li[i]
j = i-1 # j指的是手里的牌的下标
while j >= 0 and li[j] > tmp:
li[j+1] = li[j] # 右移
j -= 1
li[j+1] = tmp
print(li)
return li
if __name__ == "__main__":
li = [2, 3, 6, 1, 0, 5, 8]
insert_sort(li)
重新改进了一下插入排序,用两两之间交换实现,不用重新定一个临时变量tmp。
def insertSort(nums):
for i in range(len(nums)-1): # 手里的第i张牌
for j in range(i+1, 0, -1): # j表示即将要插入的牌
print(j)
if nums[j] < nums[j-1]:
# 如果后面的数比前面的大,则交换
nums[j], nums[j-1] = nums[j-1], nums[j]
else:
break
# print(nums)
return nums
nums = [2,3,6,1,0,5,8]
print(insertSort(nums))
NB三人组:快排、堆排、归并
(4)快速排序
算法思想
(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 "基准"(pivot)。
(2)分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大。
(3)递归地对两个序列进行快速排序,直到序列为空或者只有一个元素 。
注:图片来源于(快速排序的搜索结果_百度图片搜索 (baidu.com))
动图解释
动图来源:(快速排序动图的搜索结果_百度图片搜索 (baidu.com))
代码实现
def partition(li,left,right):
tmp = li[left]
while left < right:
while left < right and li[right] >= tmp: # 从右边找比tmp小的数
right -= 1 # 往左走一步
li[left] = li[right] # 把右边的值写到左边的空位上
# print(li)
while left < right and li[left] <= tmp:
left += 1
li[right] = li[left] # 把左边的值写到右边的空位上
# print(li)
li[left] = tmp # or li[right] = tmp
return left # or right
def quickSort(nums,left, right):
if left<right:
mid = partition(nums, left, right)
quickSort(nums,left, mid-1)
quickSort(nums,mid+1, right)
return nums
nums = [5,7,4,6,3,1,2,9,8]
# partition(nums, 0, len(nums)-1)
left = 0
right = len(nums)-1
print(quickSort(nums, left, right))
(5)堆排序
详情可参考我的另一篇博客:python-堆_QFIUNE的博客-CSDN博客
算法思想
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
堆排序的基本思想:
(1)将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点;
(2)将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值;
(3)如此反复执行,便能得到一个有序序列了;
(4)小顶堆亦是如此。
动图解释
(1)建堆
(2)排序
动图来源:(堆排序 - 6风筝9 - 博客园 (cnblogs.com))
代码实现
def adjust(li, low, hight):
"""
:param li: 列表
:param low: 堆的根节点位置
:param hight: 堆的最后一个元素的位置
:return:
"""
i = low # i最开始指向根节点
j = 2*i + 1 # j开始是左孩子
tmp = li[low] # 把堆顶存起来
while j <= hight:
if j+1 <= hight and li[j+1] > li[j]:
j += 1 # j指向右孩子
if li[j] > tmp:
li[i] = li[j]
i = j # 往下一层看
j = 2*i + 1
else: # tmp更大,把tmp放到i的位置上
li[i] = tmp # 把tmp放到某一级领导位置上
break
else:
li[i] = tmp # 把tmp放到叶子结点上
def heapSort(li):
# 构造堆,农村包围城市
n = len(li)
for i in range((n-2)//2, -1, -1):
# i表示建堆的时候调整的部分的根的下标
adjust(li, i, n-1)
# 建堆完成
# print(li)
# 挨个输出
for i in range(n-1, -1, -1):
# i指向当前堆的最后一个元素
li[0], li[i] = li[i], li[0]
adjust(li, 0, i-1) # i-1是新的high
li = [i for i in range(10)]
import random
random.shuffle(li)
print(li)
heapSort(li)
print(li)
(6)归并排序
算法思想
(1)把长度为n的输入序列分成两个长度为n/2的子序列;
(2)对这两个子序列分别采用归并排序;
(3)将两个排序好的子序列合并成一个最终的排序序列。
图片来源:(归并排序的搜索结果_百度图片搜索 (baidu.com))
动图解释
动图来源((五)归并排序法 - 简书 (jianshu.com))
代码实现
def merge(nums, low, mid, high):
ans = []
i = low
j = mid + 1
while i <= mid and j <= high:
if nums[i] <= nums[j]:
ans.append(nums[i])
i += 1
else:
ans.append(nums[j])
j += 1
while i <= mid:
ans.append(nums[i])
i += 1
while j <= high:
ans.append(nums[j])
j += 1
nums = ans
return nums
def mergeSort(nums, low, high):
if low < high:
mid = (low + high) // 2
mergeSort(nums, low, mid)
mergeSort(nums, mid+1, high)
merge(nums, low, mid, high)
# return res
nums = [5,7,4,6,3,1,2,9,8]
# partition(nums, 0, len(nums)-1)
low = 0
high = len(nums)-1
print(mergeSort(nums, low, high))
其他排序:希尔、计数、桶、基数
(7)希尔排序
希尔排序是基于直接插入排序的以下两点性质而提出的改进方法:
1.插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
2.插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。原文链接:https://www.jianshu.com/p/d730ae586cf3
算法思想
(1)希尔排序是将待排序的数组元素按下标的一定增量分组 ,分成多个子序列;
(2)然后对各个子序列进行直接插入排序算法排序;
(3)然后依次缩减增量再进行排序,直到增量为1时,进行最后一次直接插入排序,排序结束。
图片来源:(图解排序算法(二)之希尔排序 - dreamcatcher-cx - 博客园 (cnblogs.com))
动图解释
代码实现
def insertSort(nums):
for i in range(len(nums)-1): # 手里的第i张牌
for j in range(i+1, 0, -1): # j表示即将要插入的牌
print(j)
if nums[j] < nums[j-1]:
# 如果后面的数比前面的大,则交换
nums[j], nums[j-1] = nums[j-1], nums[j]
else:
break
# print(nums)
return nums
# nums = [2,3,6,1,0,5,8]
# print(insertSort(nums))
def shellSort(nums):
gap = len(nums) // 2
while gap >= 1:
for i in range(gap, len(nums)):
# i表示即将要插入的牌
# 用i表示出手里已有的牌nums[i-gap]
while i >= gap:
if nums[i-gap] > nums[i]:
nums[i-gap], nums[i] = nums[i], nums[i-gap]
i = i - gap
else:
break
gap = gap // 2
return nums
nums = [2,3,6,1,0,5,8]
print(shellSort(nums))
(8)计数排序
算法思想
用这个方法有一个前提:已知序列的范围
(1)找出原数组中元素值最大的,记为max
;
(2)创建一个新数组count
,其长度是max
加1,其元素默认值都为0;
(3)遍历原数组中的元素,以原数组中的元素作为count
数组的索引,以原数组中的元素出现次数作为count
数组的元素值;
(4)创建结果数组result
,起始索引index
;
(5)遍历count
数组,找出其中元素值大于0的元素,将其对应的索引作为元素值填充到result
数组中去,每处理一次,count
中的该元素值减1,直到该元素值不大于0,依次处理count
中剩下的元素;
(6)返回结果数组result
。
动图解释
代码实现
# 前提是已知序列的范围
# 缺点, 如果最大值是1亿,就得开1亿那么长的数组
def countSort(nums, maxValue):
count = [0 for _ in range(maxValue+1)]
# print(count)
for val in nums:
count[val] += 1
nums.clear() # 清空nums
for key,val in enumerate(count):
for i in range(val):
nums.append(key)
return nums
import random
nums = [random.randint(0,10) for _ in range(10)]
print(nums)
print(countSort(nums, 10))
(9)桶排序
算法思想
桶排序的思想近乎彻底的 分治思想 。算法的过程描述如下:
(1)根据待排序集合中最大元素和最小元素的差值范围和映射规则,确定申请的桶个数;
(2)遍历待排序集合,将每一个元素移动到对应的桶中;
(3)对每一个桶中元素进行排序,并移动到已排序集合中。
动图解释
代码实现
def bucketSort(nums, n, maxValue):
buckets = [[] for _ in range(n)] # 创建n个桶
for var in nums:
# i 表示var放到几号桶
i = min(var // (maxValue // n), n-1)
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
new_nums = []
for bucket in buckets:
new_nums.extend(bucket)
return new_nums
import random
nums = [random.randint(0,100) for i in range(100)]
# print(nums)
maxvalue = 100
n = 10
sorted_nums = bucketSort(nums, n, maxvalue)
print(sorted_nums)
(10)基数排序
算法思想
(1)取得序列中的最大数,并取得位数;
(2)nums为原始序列,从最低位开始取每个位组成radix数组;
(3)对radix进行计数排序(利用计数排序适用于小范围数的特点)。
图片来源:(基数排序的搜索结果_百度图片搜索 (baidu.com))
动图解释
动图来源:(基数排序动画的搜索结果_百度图片搜索 (baidu.com))
代码实现
# 时间复杂度: O(kn), 其中klog(10,n)
def radixSort(nums):
max_num = max(nums) # 最大值9——>1; 99——>2;888——>3;10000——>5
# 找最大的位数
it = 0
while 10 ** it <= max_num:
# 0,1,2,3,4,5,6,7,8,9十个桶
buckets = [[] for _ in range(10)]
for num in nums:
# 取位数
digit = (num//10**it)%10
buckets[digit].append(num)
# 分桶完成
nums.clear()
for bucket in buckets:
nums.extend(bucket) # 把数重新写回li
it += 1
import random
nums = list(range(100))
random.shuffle(nums)
radixSort(nums)
print(nums)