排序(有小到大排)
1.1 冒泡排序
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序算法的运作如下:
- 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
def bubble_sort(a_list):
length = len(a_list)
for i in range(length - 1):
count = 0
for j in range(length - i - 1):
if a_list[j] > a_list[j + 1]:
a_list[j], a_list[j + 1] = a_list[j + 1], a_list[j]
count += 1
if count == 0:
break
1.2 选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 n 个元素的列表表进行排序总共进行至多 n-1 次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
def selection_sort(a_list):
length = len(a_list)
for i in range(length - 1):
min_index = i
for j in range(i + 1, length):
if a_list[j] < a_list[min_index]:
min_index = j
if i != min_index:
a_list[min_index], a_list[i] = a_list[i], a_list[min_index]
1.3 插入排序
插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
def insert_sort(a_list):
length = len(a_list)
for i in range(1, length):
for j in range(i, 0, -1):
if a_list[j] < a_list[j - 1]:
a_list[j], a_list[j - 1] = a_list[j - 1], a_list[j]
else:
break
1.4 希尔排序
希尔排序 (Shell Sort) 是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减至 1 时,整个列表恰被分成一组,算法便终止。
def shell_sort(a_list):
length = len(a_list)
gap = length // 2
while gap >= 1:
for i in range(gap, length):
j = i
while j >= gap and a_list[j - gap] > a_list[j]:
a_list[j], a_list[j - gap] = a_list[j - gap], a_list[j]
j -= gap
gap //= 2
1.5 归并排序
归并排序是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解列表,再合并列表。
将列表分解最小之后,然后合并两个有序列表,基本思路是比较两个列表的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个列表为空,最后把另一个列表的剩余部分复制过来即可。
def merge(list1, list2):
m = len(list1)
n = len(list2)
i = j = 0
result = []
while i < m and j < n:
if list1[i] <= list2[j]:
result.append(list1[i])
i += 1
else:
result.append(list2[j])
j += 1
if i == m:
result += list2[j:]
else:
result += list1[i:]
return result
def merge_sort(a_list):
length = len(a_list)
if length <= 1:
return a_list
mid = length // 2
left = merge_sort(a_list[:mid])
right = merge_sort(a_list[mid:])
return merge(left, right)
1.6 快速排序
快速排序(Quick Sort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
步骤为:
- 从数列中挑出一个元素,称为"基准"(pivot)
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
def partition(l, left, right):
pivot = l[right]
i = left - 1
for j in range(left, right):
if l[j] < pivot:
i += 1
l[i], l[j] = l[j], l[i]
i += 1
l[i], l[right] = l[right], l[i]
return i
def quick_sort(a_list, left, right):
if left >= right:
return
p = partition(a_list, left, right)
quick_sort(a_list, left, p-1)
quick_sort(a_list, p+1, right)
1.7 堆排序
升序使用最大堆,最大堆是一个特殊的完全二叉树。
完全二叉树:假设二叉树的层数为 h,出了最后一层,每一层的节点数都达到了最大个数,第 h 层中节点都连续集中在最左边。
最大堆:父节点中的值比左右子树的值都大。即树中值最大的节点就是根节点。
堆排序的意思就是:构建一个最大堆,然后将根节点与最下面最右边节点交换。这个时候可能破坏最大堆的性质,因此调整使其再次成为(不在考虑先前交换的最大节点)最大堆。以此交替进行,最后得到升序结果。
def build_heap(a_list, i, length):
left = i * 2 + 1
right = i * 2 + 2
max_index = i
if left < length and a_list[left] > a_list[max_index]:
max_index = left
if right < length and a_list[right] > a_list[max_index]:
max_index = right
if max_index != i:
a_list[max_index], a_list[i] = a_list[i], a_list[max_index]
build_heap(a_list, max_index, length)
def heap_sort(a_list):
length = len(a_list)
for i in range(length // 2 - 1, -1, -1):
build_heap(a_list, i, length)
for j in range(length-1, 0, -1):
a_list[0], a_list[j] = a_list[j], a_list[0]
build_heap(a_list, 0, j)
1.8 计数排序
计数排序是一个非基于比较的排序算法。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n + k)(其中 k 是整数的范围)。
排序过程:
- 根据待排序集合中最大元素和最小元素的差值范围,申请额外空间;
- 遍历待排序集合,将每一个元素出现的次数记录到元素值对应的额外空间内;
- 根据额外空间还原回已排序的数组;
def count_sort(a_list):
max_value, min_value = max(a_list), min(a_list)
count = [0] * (max_value - min_value + 1)
for value in a_list:
count[value - min_value] += 1
res = []
for i in range(max_value - min_value + 1):
if count[i] > 0:
res += [min_value + i] * count[i]
return res
1.9 桶排序
桶排序是将待排序集合中处于同一个值域的元素存入同一个桶中,也就是根据元素值特性将集合拆分为多个区域,则拆分后形成的多个桶,从值域上看是处于有序状态的。对每个桶中元素进行排序,则所有桶中元素构成的集合是已排序的。
快速排序是将集合拆分为两个值域,这里称为两个桶,再分别对两个桶进行排序,最终完成排序。桶排序则是将集合拆分为多个桶,对每个桶进行排序,则完成排序过程。两者不同之处在于,快排是在集合本身上进行排序,属于原地排序方式,且对每个桶的排序方式也是快排。桶排序则是提供了额外的操作空间,在额外空间上对桶进行排序,避免了构成桶过程的元素比较和交换操作,同时可以自主选择恰当的排序算法对桶进行排序。
当然桶排序更是对计数排序的改进,计数排序申请的额外空间跨度从最小元素值到最大元素值,若待排序集合中元素不是依次递增的,则必然有空间浪费情况。桶排序则是弱化了这种浪费情况,将最小值到最大值之间的每一个位置申请空间,更新为最小值到最大值之间每一个固定区域申请空间,尽量减少了元素值大小不连续情况下的空间浪费情况。
参考桶排序
def bucket_sort(a_list):
max_value, min_value = max(a_list), min(a_list)
length = len(a_list)
bucket_each = (max_value - min_value) / length
count = [[] for _ in range(length + 1)]
for value in a_list:
count[int((value - min_value) // bucket_each)].append(value)
res = []
for i in count:
for j in sorted(i):
res.append(j)
return res
1.10 基数排序
基数排序也可以称为多关键字排序,同计数排序类似,也是一种非比较性质的排序算法。将待排序集合中的每个元素拆分为多个总容量空间较小的对象,对每个对象执行桶排序后,则完成排序过程。
基数排序过程中也使用了桶排序操作,不过对于桶排序面向的对象进行了优化。例如,若元素是整数类型,则选择元素的每位数字作为排序对象,因为每个数字的容量空间大小只是 10;所以在基数排序过程中,给其中的桶排序操作选择了容量空间有限的排序对象。
基数排序中的桶排序操作具有一点特殊性,即每个桶的宽度,或者称为值域跨度为一,所以将待排序集合中所有元素移动到各个桶上之后,不需要再对每个桶进行排序。
算法过程
- 根据待排序元素的类型申请桶空间,并从待排序集合中计算出元素的最大位数;
- 从右向左,根据元素当前位数的值,将所有元素移动到对应的桶中;
- 将所有桶中元素移动回原始集合中;
- 重复步骤 2, 3,直到遍历完所有位数。
参考基数排序
def radix_sort(a_list):
i = 0
max_length = len(str(max(a_list)))
while i < max_length:
bucket = {}
for j in range(10):
bucket.setdefault(j, [])
for value in a_list:
radix = int(value / (10**i) % 10)
bucket[radix].append(value)
j = 0
for k in range(10):
if len(bucket[k]) != 0:
for y in bucket[k]:
a_list[j] = y
j += 1
i += 1
2. 时间复杂度分析
方法 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 | 是否原地 |
---|---|---|---|---|---|---|
冒泡排序 | O( n 2 n^2 n2) | O( n 2 n^2 n2) | O( n n n) | O( 1 1 1) | 稳定 | 原地 |
选择排序 | O( n 2 n^2 n2) | O( n 2 n^2 n2) | O( n 2 n^2 n2) | O( 1 1 1) | 不稳定 | 原地 |
插入排序 | O( n 2 n^2 n2) | O( n 2 n^2 n2) | O( n n n) | O( 1 1 1) | 稳定 | 原地 |
希尔排序 | O( n 1.3 n^{1.3} n1.3) | O( n 2 n^2 n2) | O( n n n) | O( 1 1 1) | 不稳定 | 原地 |
归并排序 | O( n l o g n nlogn nlogn) | O( n l o g n nlogn nlogn) | O( n l o g n nlogn nlogn) | O( n n n) | 稳定 | 不原地 |
快速排序 | O( n l o g n nlogn nlogn) | O( n 2 n^2 n2) | O( n l o g n nlogn nlogn) | O( l o g n logn logn) | 不稳定 | 原地 |
堆排序 | O( n l o g n nlogn nlogn) | O( n l o g n nlogn nlogn) | O( n l o g n nlogn nlogn) | O( 1 1 1) | 不稳定 | 原地 |
计数排序 | O( n + k n+k n+k) | O( n + k n+k n+k) | O( n + k n+k n+k) | O( n + k n+k n+k) | 稳定 | 不原地 |
桶排序 | O( n + k n+k n+k) | O( n 2 n^2 n2) | O( n n n) | O( n + k n+k n+k) | 稳定 | 不原地 |
基数排序 | O( n k nk nk) | O( n k nk nk) | O( n k nk nk) | O( n + r n+r n+r) | 稳定 | 不原地 |
计数排序中 k 为辅助计数数组 count 的长度;
桶排序中 k 为痛的个数;
基数排序中 k 为元素的最大位数,r 为元素的基数;