排序

经典排序:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。

奇葩排序(名字没听过,所以感觉有点奇葩):猴子排序、睡眠排序、面条排序。

排序算法优越评价有三个指标,执行效率、内存消耗、稳定性,一般来讲,在分析效率时会从几个方面来衡量:


  1. 时间复杂度。会从最好、最坏和平均情况三个来分析;
  2. 时间复杂度的系数、常数 、低阶。在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。
  3. 比较次数和交换(或移动)次数。

内存消耗:原地排序(Sorted in place)。 原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。

稳定性: 这个是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。

1. 冒泡排序(Bubble Sort)

冒泡排序是最基本最简单的排序了,在大家刚开始学习 C 语言的时候就会接触到。基本的思想就是,对于一个数比较与之相邻的数字,例如要把一个数列按从小到大的顺序排列,就拿左边第一个数,和第二的比,若小于第二个数两个交换,否则不换,再比较第二个和第三个,按照同样的规则,继续第三第四…直到最后。这样就算一次冒泡,每次冒泡都会有一个数被放到了最终的位置。就如下图中这样,图中的下边的数就是我所说的左边的数。

冒泡排序、插入排序、选择排序_时间复杂度

所以一个 n 长度的数列,至多就只要进行 n 次的冒泡就可以了。实际上,当其中一次冒泡操作没有数据交换时,就可以结束排序了。

# Copyright (c) strongnine

def bubbleSort(a):
## 读取输入数组的大小
n = len(a)

if n <= 1:
return

for i in range(n):
## 提前退出冒泡循环的标志位
flag = False

for j in range(n - i -1):
## 交换
if (a[j] > a[j + 1]):
a[j], a[j + 1] = a[j + 1], a[j]
## 为 True 表示这次冒泡有数据交换
flag = True

## 没有数据交换,提前退出
if not flag:
break

if __name__ == '__main__':
## 对上面说的两个数据进行测试
a = [4, 5, 6, 3, 2, 1]
b = [3, 5, 4, 1, 2, 6]
bubbleSort(a)
bubbleSort(b)
print('a = ', a , '\nb = ', b)

冒泡排序是原地排序的稳定的算法,时间复杂度为 O(n2)

2. 插入排序(Insertion Sort)

插入排序把数组分为已排序区和未排序区。取未排序区的元素,在已排序区上找到一个正确的位置插上去。还是希望对一个数据进行从小到大的排序。我们从未排序区上拿一个元素,按从右到左与已排序区的元素对比,如果如果当前元素 A 小于已排序区中的元素 B,让 B 往后移,即让 B 后面的位置等于 B,继续比 B 前面的数,也叫它为 B,它是新的一个 B,重复操作直到 A 大于 B,就让 A 插进当前 B 的前面。

冒泡排序、插入排序、选择排序_时间复杂度_02

# Copyright (c) strongnine

def insertionSort(a):
n = len(a)

if n <= 1:
return

for i in range(1, n):
value = a[i]
j = i - 1
## 查找插入的位置
for j in range(j, -2, -1):
if a[j] > value:
## 数据移动
a[j + 1] = a[j]
else:
break
## 插入数据
a[j + 1] = value


if __name__ == '__main__':
a = [4, 5, 6, 1, 3, 2]
insertionSort(a)
print('a = ', a)

插入排序是原地排序的稳定的算法,时间复杂度为 O(n2)

3. 选择排序(Selection Sort)

选择排序也会把数组分为已排序区和未排序区。但是与插入排序不同的是,它每次找到未排序区的最小值,与未排序区的首个元素交换,这样就变成了已排序区的末尾元素了。

冒泡排序、插入排序、选择排序_冒泡排序_03

# Copyright (c) strongnine

def selectionSort(a):
n = len(a)

if n <= 1:
return

for i in range(n):
## 用于比较
index = i
## 得到最小值下标
for j in range(i + 1, n):
if a[index] > a[j]:
index = j
## 交换
a[i], a[index] = a[index], a[i]


if __name__ == '__main__':
a = [4, 5, 6, 3, 2, 1]
selectionSort(a)
print('a = ', a)

需要注意的是,由于选择排序可能会把一个元素交换到与其数值相同的元素的后面,所以它不是一个稳定的算法。例如对于 [3, 4, 5, 6, 3, 1],在第一次交换中,第一个 3 就会和 1 交换,它会变到最后面,这样两个 3 的顺序就变化了。

冒泡排序与插入排序

为何在实际中倾向于使用插入排序而不是冒泡排序,尽管它们的时间复杂度都是O(n2),而且也都是稳定的。看一下两个算法在交换元素数值的处理上就知道了。

对于冒泡排序,交换两个元素时需要引入中间变量,也就是如果需要交换 A 和 B,我们需要让 A 赋值给 C,然后让 A 等于 B,再让 B 等于 C。而插入排序在每次比较时会把大的元素往后移,要插入的时候直接插入,所以更加的直接,在实际应用时更常用。

在 Python 上测试一下也可以知道,冒泡排序比插入排序的时间花费更多。我分别使用两种算法对一个 numpy 生成的长度为 2000 随机数组进行排序并计算时间,发现冒泡排序花费了 1 秒钟的时间,而插入排序只需使用 0.5 秒。

a = np.random.randint(10000, size=2000)

tick = time.time()
bubbleSort(a)
tock = time.time()
print('bubbleSort takes', tock - tick, 's.')

tick = time.time()
selectionSort(a)
tock = time.time()
print('selectionSort takes', tock - tick, 's.')

>>> bubbleSort takes 1.032163381576538 s.
>>> selectionSort takes 0.5756876468658447 s.