十种常见排序算法可以分为两大类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
算法复杂度:
1. 比较类排序
1.1. 交换排序
1.1.1. 冒泡排序(交换邻接元素)
平均时间复杂度 | 最差时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n2) | O(n2) | O(n) | O(1) | 稳定 |
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
def bubbleSort(arr):
n = len(arr)
# 遍历所有数组元素
for i in range(n):
# Last i elements are already in place
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
arr = [64, 34, 25, 12, 22, 11, 90]
bubbleSort(arr)
print("排序后的数组:", arr)
1.1.2. 快速排序(交换left/right指针指向元素)
平均时间复杂度 | 最差时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(nlog2n)) | O(n2) | O(nlog2n) | O(nlog2n) | 不稳定 |
note: 交换左右指针指向元素时无需移动左右指针
def quick_sort(L):
return q_sort(L, 0, len(L) - 1)
def q_sort(L, left, right):
if left < right:
pivot = Partition(L, left, right)
q_sort(L, left, pivot - 1)
q_sort(L, pivot + 1, right)
return L
def Partition(L, left, right):
pivotkey = L[left]
while left < right:
while left < right and L[right] >= pivotkey:
right -= 1
L[left] = L[right]
while left < right and L[left] <= pivotkey:
left += 1
L[right] = L[left]
L[left] = pivotkey
return left
L = [5, 9, 1, 11, 6, 7, 2, 4]
print(quick_sort(L))
1.2. 插入排序
1.2.1. 简单插入排序
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
**适用:**少量数据的排序,
**时间复杂度:**O(n^2)
稳定
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素(已排序)移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
def SimpleInsertSort(arr):
len_a = len(arr)
if len_a <= 1:
return arr
for i in range(1, len_a):
j = i
temp = arr[i]
while j > 0 and temp < arr[j - 1]:
arr[j] = arr[j - 1]
j -= 1
arr[j] = temp # 把temp插到空位
return arr
arr = [3, 1, 2, 4, 6, 7, 8, 9, 5]
arr = SimpleInsertSort(arr)
print(arr) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
1.2.2. 希尔排序(简单插入排序算法的高效改进版本)
- 排序数组A=[8 9 1 7 2 3 5 4 6 0]
- 初始增量为 gap = 10/2 = 5,整个数组分成了 5 组【 8 , 3 】,【 9 , 5 】,【 1 , 4 】,【 7 , 6 】,【 2 , 0 】。对这分开的 5 组分别使用简单插入排序,得到 3 5 1 6 0 8 9 4 7 2
- 缩小增量 gap = 5/2 = 2,整个数组分成了 2 组【 3 , 1 , 0 , 9 , 7 】,【 5 , 6 , 8 , 4 , 2 】。对这分开的 2 组分别使用插入排序,得到 0 2 1 4 3 5 7 6 9 8
- 再缩小增量 gap = 2/2 = 1,整个数组分成了 1 组【 0, 2 , 1 , 4 , 3 , 5 , 7 , 6 , 9 , 8 】。进行插入排序,得到 0 1 2 3 4 5 6 7 8 9
def ShellInsertSort(arr):
len_a = len(arr)
if len_a <= 1:
return arr
d = len_a // 2
while d >= 1:
for i in range(d, len_a):
j = i - d
temp = arr[i]
while (j >= 0 and arr[j] > temp): # 从后向前,找比其小的数的位置
arr[j + d] = arr[j] # 向后挪动
j -= d
if j != i - d:
arr[j + d] = temp
d = d // 2
return arr
arr = [50, 16, 30, 10, 60, 90, 2, 80, 70]
arr = ShellInsertSort(arr)
print(arr) # [2, 10, 16, 30, 50, 60, 70, 80, 90]
1.3. 选择排序
从无序区中选择最大/最小元素,放入有序区末尾1.3.1. 简单选择排序
平均时间复杂度 | 最差时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n2) | O(n2) | O(n2) | O(1) | 稳定 |
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。实现从小到大排序的具体算法描述如下:
- 初始状态:有序区为空,无序区为R[1…n];
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R[i…n]。该趟排序从当前无序区R[i…n]中选出最小值 R[k],将它与无序区的第1个记录R[i]交换,使R[1…i-1]和R[i…n]分别变为记录个数增加1个的新有序区R[1…i]和记录个数减少1个的新无序区R[i+1…n];
- n-1趟结束,数组实现从小到大排列。
python代码实现
def SimpleChooseSort(arr):
for j in range(len(arr)-1):
minIndex = j
for i in range(j+1, len(arr), 1):
if arr[i] < arr[minIndex]:
minIndex = i
arr[j], arr[minIndex] = arr[minIndex], arr[j]
arr = [3,1,2,4,6,7,8,9,5]
SimpleChooseSort(arr)
print(arr)
1.3.2. 堆排序
- 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆(升序选择大顶堆,降序选择小顶堆);
- 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
- 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
具体过程
from collections import deque
def swap_param(L, i, j):
L[i], L[j] = L[j], L[i]
return L
def heap_adjust(L, start, end):
temp = L[start]
i = start
j = 2 * i
while j <= end:
if (j < end) and (L[j] < L[j + 1]):
j += 1
if temp < L[j]:
L[i] = L[j]
i = j
j = 2 * i
else:
break
L[i] = temp
def heap_sort(L):
L_length = len(L) - 1
first_sort_count = L_length // 2
for i in range(first_sort_count):
heap_adjust(L, first_sort_count - i, L_length)
for i in range(L_length - 1):
L = swap_param(L, 1, L_length - i)
heap_adjust(L, 1, L_length - i - 1)
return [L[i] for i in range(1, len(L))]
def main():
L = deque([50, 16, 30, 10, 60, 90, 2, 80, 70])
L.appendleft(0)
print(heap_sort(L))
main()
1.4. 归并排序
1.4.1. 二路归并排序
算法思路:
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
# 归并排序(分)
def mergeSort(arr):
if len(arr) < 2:
return arr
mid = len(arr) // 2
left = mergeSort(arr[:mid])
right = mergeSort(arr[mid:])
return merge(left, right)
# 归并排序(治)
def merge(left, right):
if not len(left) or not len(right):
return left or right
result = []
i, j = 0, 0
while (len(result) < len(left) + len(right)):
if left[i] < right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
if i == len(left) or j == len(right):
result.extend(right[j:] or left[i:])
break
return result
arr = [50, 16, 30, 10, 60, 90, 2, 80, 70]
arr = mergeSort(arr)
print(arr)
1.4.2. 多路归并排序
2. 非比较类排序
2.1. 计数排序
待排序数组A,算法描述:
- O(n)的时间扫描一下整个序列 A,获取最小值 min 和最大值 max
- 开辟一块新的空间创建新的数组 B,长度为 ( max - min + 1)
- 数组 B 中 index (index = A中元素 - min)的元素记录的值是 A 中某元素出现的次数
- 最后输出目标整数序列,具体的逻辑是遍历数组 B,输出相应元素(输出元素值 = B中某元素 + min)以及对应的个数
例如:待排序数组A=[5 3 4 7 2 4 3 4 7]
得到min = 2, max = 7, len(B) = max - min + 1 = 6, B[0] = 1, B[1] = 2, B[2] = 3, B[3] = 1, B[4] = 0, B[5] = 2.
遍历数组B, 输出排序后的A = [2,3,3,4,4,4,5,7,7]
2.2. 桶排序
基本思想:假设有一组长度为N的待排关键字序列K[1…n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]…B[M]中的全部内容即是一个有序序列。
例如:
假如待排序列K= {49,38,35,97,76,73,27,49}。这些数据全部在1 - 100之间。因此我们定制10个桶,然后确定映射函数f(k) = k/10。则第一个关键字49将定位到第4个桶中(49/10 = 4)。依次将所有关键字全部堆入桶中,并在每个非空的桶中进行快速排序。
2.3. 基数排序
基本思想:将数组中的所有数按位进行分类,由于每一位数的大小都在0 - 9之间,因此创建下标为0 - 9的十个数组,根据需要对数进行存储。
例如:对于一个数组:[8,9,6,11,23,1,9,18,10,213,33,7,87,91,180,35,52,716,106]
(1)选择个位数相同的元素,并成为一个数组
(2)选择十位数为i的数的元素,并成为一个数组
(3)选择百位数为i的数的元素,并成为一个数组