冒泡排序选择排序插入排序希尔排序(对插入排序的改进)归并排序快速排序(对冒泡排序的改进)堆排序计数排序桶排序基数排序时间复杂度对比
冒泡排序
-
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
-
对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
-
针对所有的元素重复以上的步骤,除了最后一个。
-
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
nums = [3, 7, 2, 8, 1, 4, 6, 5]
# 外层循环控制循环次数
for i in range(1, len(nums)):
# 内层循环控制比较次数
for j in range(len(nums)-i):
if nums[j] > nums[j+1]:
nums[j], nums[j+1] = nums[j+1], nums[j]
print(nums)
选择排序
-
从待排序列表中找到最小的元素(升序排列,降序排列则找最大的元素),存放到列表的起始位置,作为已排序的序列。
-
继续从未排序序列中找到最小的元素,存放到已排序序列的末尾(同时也是未排序序列的起始位置)。
-
重复第2步,直到所有元素都已经存放到了已排序序列,则列表排序完成。
-
第一次遍历找到最小元素
-
第二次在剩余数组中遍历找到次小元素
-
···
-
第n次在剩余数组中遍历找到第n小元素
# 外层循环控制循环次数
nums = [3, 7, 2, 8, 1, 4, 6, 5]
for i in range(len(nums)-1):
min_idx = i
# 内层循环找出最小元素
for j in range(i+1, len(nums)):
if nums[j] < nums[min_idx]:
min_idx = j
# 与第一个元素交换位置
nums[i], nums[min_idx] = nums[min_idx], nums[i]
print(nums)
插入排序
-
将待排序列表的第一个元素当做已排序序列,第二个元素到最后一个元素当成未排序序列。
-
取未排序序列中的第一个数据,插入到已排序序列中顺序正确的位置。将未排序的第一个数据与相邻的前一个数据(已排序序列的最后一个数)据进行比较,如果顺序错误则交换位置,交换位置后继续与相邻的前一个数据进行比较,直到不需要交换则插入完成。每次插入数据后,已排序序列都是排好序的。
-
重复上一步,继续插入下一个数据。每进行一次插入,已排序序列的长度加1,未排序序列的长度减1,直到列表中的所有数据都插入到已排序序列了,则列表排序完成。
-
将数组待排序元素依次插入到已排序部分,使已排序部分保持升序的性质
nums = [3, 7, 2, 8, 1, 4, 6, 5]
for i in range(1, len(nums)):
j = i
# 将遍历到的元素依次插入到该元素左边序列中
while j > 0 and nums[j] < nums[j-1]:
nums[j], nums[j-1] = nums[j-1], nums[j]
# 继续往前遍历
j -= 1
print(nums)
希尔排序(对插入排序的改进)
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止
nums = [3, 7, 2, 8, 1, 4, 6, 5]
n = len(nums)
# 定义增量
gap = n // 2
while gap:
for i in range(gap, n):
# 分组插入排序
while i >= gap and nums[i] < nums[i-gap]:
nums[i], nums[i-gap] = nums[i-gap], nums[i]
i -= gap
# 缩小增量
gap //= 2
print(nums)
归并排序
-
假设有两个已经有序的列表,设定两个指针分别指向这两个列表的起始元素,申请内存空间新建一个空列表,比较两个指针指向的元素大小,将较小的元素添加到新列表中,然后将该指针向该列表的下一个元素偏移,继续比较两个指针指向的元素和添加较小的到新列表中。直到其中一个列表的数据全部被添加完时,把另一个列表中剩下的数据按顺序添加到新列表中。这就实现了将两个有序列表合并成一个新的有序列表的方法。
-
对待排序列表进行拆分,递归地拆分直到子列表中只有一个元素。
-
只有一个元素的子列表一定是有序的,使用1中的方法对有序的子列表进行合并。第一次合并后新列表是有两个元素的有序列表,递归地往回合并,直到所有数据都合并到一个新的有序列表中,列表排序完成。
分而治之:分解数组 递归求解 合并排序
-
分解原问题: 将数组𝑨[𝟏, 𝒏] 排序问题分解为𝑨[𝟏, 𝒏 / 𝟐 ]和𝑨[ 𝒏 / 𝟐 + 𝟏, 𝒏]排序问题
-
解决子问题:递归解决子问题得到两个有序的子数组
-
合并问题解:将两个有序子数组合并为一个有序数组
nums = [3, 7, 2, 8, 1, 4, 6, 5]
def merge_sort(nums):
# 递归结束条件
if len(nums) < 2:
return nums
left = merge_sort(nums[:len(nums)//2])
right = merge_sort(nums[len(nums)//2:])
return merge(left, right)
def merge(left, right):
l = r = 0
res = []
while l < len(left) and r < len(right):
if left[l] < right[r]:
res.append(left[l])
l += 1
else:
res.append(right[r])
r += 1
# 添加剩余部分
res += left[l:]
res += right[r:]
return res
print(merge_sort(nums))
快速排序(对冒泡排序的改进)
-
从待排序列表中选取一个基准数据(通常选取第一个数据)。
-
将待排序列表中所有比基准数据小的元素都放到基准数据左边,所有比基准数据大的元素都放到基准数据右边(升序排列,降序反之)。用基准数据进行分割操作后,基准数据的位置就是它最终排序完成的位置,第一轮排序完成。
-
递归地对左右两个部分的数据进行快速排序。即在每个子列表中,选取基准,分割数据。直到被分割的数据只有一个或零个时,列表排序完成。
-
首先设定一个分界值,通过该分界值将数组分成左右两部分
-
将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于分界值,而右边部分中各元素都大于或等于分界值
-
然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理
-
重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了
nums = [3, 7, 2, 8, 1, 4, 6, 5]
def quick_sort(li):
if len(li) < 2:
return li
mid = li.pop(len(li)//2)
l, r = [], []
for x in li:
if x < mid:
l.append(x)
else:
r.append(x)
return quick_sort(l) + [mid] + quick_sort(r)
# 逐一赋值
def quick_sort(nums):
l, r = 0, len(nums)-1
# 数组为空或只有一个元素
if l >= r:
return nums
# 定义初始值
key = nums[l]
while l < r:
# 找到小的值放到左边
while l < r and nums[r] >= key:
r -= 1
nums[l] = nums[r]
# 找到大的值放到右边
while l < r and nums[l] <= key:
l += 1
nums[r] = nums[l]
# 将初始值赋值给相遇的位置
nums[l] = key
return quick_sort(nums[:l]) + [key] + quick_sort(nums[l+1:])
# 两者交换
def quick_sort(nums):
l, r = 0, len(nums)-1
if l >= r:
return nums
# 定义初始索引
p = l
while l < r:
while l < r and nums[r] >= nums[p]:
r -= 1
while l < r and nums[l] <= nums[p]:
l += 1
# 用索引交换
nums[l], nums[r] = nums[r], nums[l]
nums[l], nums[p] = nums[p], nums[l]
return quick_sort(nums[:l]) + [nums[l]] + quick_sort(nums[l+1:])
堆排序
-
将待排序列表中的数据按从上到下、从左到右的顺序构造成一棵完全二叉树。
-
将完全二叉树中每个节点(叶节点除外)的值与其子节点(子节点有一个或两个)中较大的值进行比较,如果节点的值小于子节点的值,则交换他们的位置(大顶堆,小顶堆反之)。
-
将节点与子节点进行交换后,要继续比较子节点与孙节点的值,直到不需要交换或子节点是叶节点时停止。比较完所有的非叶节点后,即可构建出堆结构。
-
将数据构造成堆结构后,将堆顶与堆尾交换,然后将堆尾从堆中取出来,添加到已排序序列中,完成一轮堆排序,堆中的数据个数减1。
-
重复步骤2,3,4,直到堆中的数据全部被取出,列表排序完成。
nums = [3, 7, 2, 8, 1, 4, 6, 5]
def heap_sort(nums):
n = len(nums)
# 最后一个非叶节点的索引
first = n//2-1
# 构造最大堆
for start in range(first, -1, -1):
max_heapify(nums, start, n-1)
# 将最大堆转化为有序数组
for end in range(n-1, 0, -1):
# 将堆顶和堆尾换过来
nums[end], nums[0] = nums[0], nums[end]
# 重新调整成最大堆
max_heapify(nums, 0, end-1)
return nums
# 大顶堆
def max_heapify(nums, start, end):
root = start
while True:
# 左子节点的索引
child = root*2+1
if child > end:
break
# 获取左右子节点的较大者
if child+1 <= end and nums[child] < nums[child+1]:
child += 1
# 要交换
if nums[root] < nums[child]:
nums[root], nums[child] = nums[child], nums[root]
# 将根节点换为子节点继续构造
root = child
else:
break
# 小顶堆
def min_heapify(nums, start, end):
root = start
while True:
child = root*2+1
if child > end:
break
if child+1 <= end and nums[child] > nums[child+1]:
child += 1
if nums[root] > nums[child]:
nums[root], nums[child] = nums[child], nums[root]
root = child
else:
break
计数排序
-
找到待排序列表中的最大值 k,开辟一个长度为 k+1 的计数列表,计数列表中的值都为 0。
-
走访待排序列表,如果走访到的元素值为 i,则计数列表中索引 i 的值加1。
-
走访完整个待排序列表,计数列表中索引 i 的值 j 表示 i 的个数为 j,统计出待排序列表中每个值的数量。
-
创建一个新列表,遍历计数列表,依次在新列表中添加 j 个 i,新列表就是排好序后的列表,整个过程没有比较待排序列表中的数据大小。
def count_sort(nums):
max_v, min_v = max(nums), min(nums)
cnt = [0] * (max_v-min_v+1)
res = [0] * len(nums)
# 记录每个元素出现次数
for x in nums:
cnt[x-min_v] += 1
# 包括当前元素在内有多少个元素在桶里
# 模拟索引
for i in range(1, len(cnt)):
cnt[i] += cnt[i-1]
for x in nums:
# 计数-1得索引
res[cnt[x-min_v]-1] = x
# 存入一个数 计数-1
cnt[x-min_v] -= 1
return res
桶排序
-
求出待排序列表中的最大值和最小值,得到数据的范围。
-
根据数据的范围,选择一个适合的值构建有限数量的桶,确定每个桶的数据范围。如数据范围是[0,100),将数据分成10个桶,第一个桶为[0,10),第二个桶为[10,20),以此类推。
-
将待排序列表中的数据分配到对应的桶中。
-
对每一个桶内的数据进行排序,这里可以采用任意一种排序算法,建议采用时间复杂度小的排序算法。
-
将所有桶中的数据依次取出,添加到一个新的有序序列中,列表排序完成。
def bucket_sort(nums):
# 找到最大值最小值
max_v, min_v = max(nums), min(nums)
# 计数数组
cnt = [0 for _ in range(min_v, max_v+1)]
res = []
for x in nums:
cnt[x-min_v] += 1
for i in range(len(nums)):
# 添加元素
if cnt[i]:
res += [i+min_v] * cnt[i]
return res
基数排序
-
求出待排序列表中的最大值,并求出最大值的位数,有多少位就需要进行多少轮分桶和合并。
-
开辟内存空间,创建用于分配数据的桶。整数排序时,每一位的范围都在0~9之间,所以需要创建10个桶。
-
从数据的个位开始(从最高位开始也可以,结果一样),按个位数对数据进行分桶,不考虑其它位的数据大小。
-
待排序列表中的所有数据都分桶完成后,将所有桶中的数据进行合并,合并时按先进先出的原则取出桶中的数据。
-
重复步骤3,4,继续按其他位对前面处理过的数据进行分桶和合并。一直到对每一位数据都进行分桶和合并完成,最终得到一个有序序列,列表排序完成。
def radix_sort(nums):
# 最大数字长度作为排序次数
for k in range(len(str(max(nums)))):
# 0-9建立10个桶
buckets = [[] for _ in range(10)]
# 从低位到高位获取对应数字 放入对应的桶
for x in nums:
buckets[x//(10**k)%10].append(x)
# 先进先出的原则取出桶中的数据
nums = [x for bucket in buckets for x in bucket]
return nums
时间复杂度对比
排序算法 | 最坏时间复杂度 | 平均时间复杂度 | 最优时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
希尔排序 | O(n^2) | O(n^1.5) | O(nlogn) | O(1) | 不稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
快速排序 | O(n^2) | O(nlogn) | O(nlogn) | O(logn) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
桶排序 | O(n^2) | O(n+k) | O(n+k) | O(n) | 取决于桶内排序 |
基数排序 | O(d(n+k)) | O(d(n+k)) | O(d(n+k)) | O(n+k) | 稳定 |