Python 实现经典排序算法
按照通俗的定义, 排序就是按照某种规则,将一组给定的数据对象(记录)排列次序,其主要的目的是提高数据检索的效率,通常数据对象有多个属性域,即由多个数据成员组成,其中一个属性域可以用来区分对象,并作为排序的依据,则该属性域就成为关键字(key),这个数据对象中的关键字的值叫做键值。
进一步明确地定义,排序是将一组数据对象(记录)按照相应的关键字的键值递增或递减的次序重新排列的过程。键值的递增排序叫做升序(又称正序), 键值的递减排列顺序称为降序(又称逆序)。
本文使用 Python 来实现常见的经典排序算法。
一. 插入排序
直接插入排序
- 将顺序存储的 n 个待排序记录划分为两个区间:一个有序区间,一个无序区间;初始时有序区间为 [R1],无序区间为 [R2...Rn],令 i 指向无序区中第一个元素,初始值 i=1。
- 当 i<=n 时重复指向: 将当前无序区中的第一个记录Ri插入到有序区的适当位置,使得有序区变为一个新的有序区。
- 当 i>n 时结束排序。
时间复杂度为O(n^^2), 空间复杂度为O(1)。
def insert_sort(target):
for i in range(1, len(target)):
tmp = target[i]
if tmp < target[i-1]:
j = i - 1
while tmp < target[j] and j >= 0:
target[j+1] = target[j]
j -= 1
target[j+1] = tmp
return target
print(insert_sort([1, -1, -2, -1, -19, 0]))
折半插入排序
从前面的直接插入排序算法中,不难看出每项都进行了如下操作:
1. 从前面的有序字表中找出待插入元素应该被插入的位置。
2. 给插入的位置腾出空间,将带插入的元素复制到表中的插入位置。
注意到该算法中,总是边比较边移动元素,下面将比较和移动操作分离出来,即先用折半查找出元素的待插入位置,然后再统一移动带插入位置之后的所有元素。
因为只减少了比较次数,移动次数并没有减少,所以时间复杂度为O(n^^2), 空间复杂度为O(1)。
def insert_sort(target):
for i in range(1, len(target)):
tmp = target[i]
low, high = 0, i-1
while low <= high:
middle = (low + high) // 2
if tmp < target[middle]:
high = middle - 1
else:
low = middle + 1
for j in range(i-1, high, -1):
target[j+1] = target[j]
target[high+1] = tmp
return target
print(insert_sort([1, -1, -2, -1, -19, 0]))
希尔排序
希尔排序(ShellPass)也叫缩小增量排序(Diminishing Increment Sort),它是插入排序的一种,它在时间复杂度上比直接插入排序要好。基本思想是:先将整个待排序列分割成若干子序列分别进行直接插入排序,待整个序列"基本有序"时,再对全体记录进行一次直接插入排序。
在希尔排序中执行时间依赖于增量序列。好的增量序列的共同特征:最后一个增量必须为1,并且应该尽量避免序列中的值互为倍数。d = floor(d/2) d = floor(d-1/2) d = floor(d+1/3) d = floor(d-1/3)
当d等于1时,希尔排序和插入排序基本一致。
基本步骤:
- 将整个待排序列以步长为d分为若干子序列,把相隔为d的记录放在同一组。
- 在每个分组类进行直接插入排序。
- 更改d的值,重复上述步骤,直到d的值为1,即所有的记录都放入了一个组中进行插入排序了。
注意:
- watch_j 的意思是保存for循环里面j的值,目的是当这个for循环里面
tmp < target[j]
全都满足时更新target[watch_j] = tmp - 注意
for else
的用法,只有for里面没有break而结束for循环时执行else语句。
时间复杂度为O(n^^1.25), 空间复杂度为O(1)。
def shell_sort(target):
d = length = len(target)
while True:
d = (d + 1) // 3
if d < 1:
return target
for i in range(d, length):
tmp = target[i]
if tmp < target[i-d]:
j = i - d
while tmp < target[j] and j >= 0:
target[j+d] = target[j]
j -= d
target[j+d] = tmp
print(shell_sort([4, 0, -1, 2, 1, -1]))
二. 交换排序
交换排序(Exchange Sort)是一种在排序过程中借助于交换操作来完成排序的方法。交换排序的基本思想时两两比较待排序记录的关键字,如果发现两个关键字逆序,则将两个记录位置互换,重复此过程直到所有关键字有序为止。
冒泡排序
冒泡排序需要两次循环,最外层是控制已经排完序的区间,内层循环控制未排完序区间的比较。
注意exchange
遍历没有也可以实现,exchange
的作用是如果某一趟没有交换过数据,那么表示这个序列已经有序了。
时间复杂度为O(n^^2), 空间复杂度为O(1)。
def bubble_sort(target):
for i in range(len(target)-1, 0, -1):
exchange = False
for j in range(i):
if target[j] > target[j+1]:
tmp = target[j]
target[j] = target[j+1]
target[j+1] = tmp
exchange = True
if not exchange:
break
return target
print(bubble_sort([4,2,1,-1]))
快速排序
快速排序采用分治法,将问题分解为更小的问题分别求解,基本思想如下:先通过一趟排序,将待排序记录分割成独立的两部分,一中一部分记录的关键字均比另外一部分关键字小,然后可分别对这两部分记录进行排序,已达到整个序列有序的目的。
因为每一次将待排序记录分割为两部分的逻辑一样,所以采用递归的方式。
时间复杂度为O(n*log2*n),n乘以log 2底n,空间复杂度为O(n)。
def quick_sort(target):
def _quick_pass(i, j):
middle_value = target[i]
while i < j:
while target[j] > middle_value and i < j:
j -= 1
if i < j:
target[i] = target[j]
i += 1
while target[i] <= middle_value and i < j:
i += 1
if i < j:
target[j] = target[i]
j -= 1
target[i] = middle_value
return i
def _quick_sort_recursive(start, end):
if start < end:
middle_index = _quick_pass(start, end)
_quick_sort_recursive(start, middle_index-1)
_quick_sort_recursive(middle_index+1, end)
_quick_sort_recursive(0, len(target)-1)
return target
print(quick_sort([4,2,1,-1]))
三. 选择排序
选择排序(Selection Sort)的基本思想是:每一趟排序(i=1,1,2,3...,n-1)在 n-i+1 个待排序记录中选出关键字最小的记录,作为有序序列的第i个记录,直到全部排序完毕为止。常用的选择排序有直接选择排序和堆排序。
直接选择排序
直接选择排序(Straight Selection Sort)又称为简单选择排序。基本思想入下:把顺序存储的n个待排序记录看成是由一个有序区和一个无序区,初始状态有序区为空,无序区为(R1,...Rn)。每一趟从无序区中找出最小的元素,将其和无序区的开头交换,这样有序区的元素就增加了一个,无序区的元素就减少了一个了。
时间复杂度为O(n^^2),空间复杂度为O(1)。
def select_sort(target):
length = len(target)
for i in range(length-1):
watch_i = i
for j in range(i+1, length):
if target[j] < target[watch_i]:
watch_i = j
if watch_i != i:
tmp = target[watch_i]
target[watch_i] = target[i]
target[i] = tmp
return target
print(select_sort([4,2,1,-1]))
堆排序
满二叉树: 在一棵二叉树中,如果所有分支节点都存在左子树和右子树,并且所有叶子节点都在同一层上,这样的一个二叉树叫做满二叉树。
完全二叉树:如果对二叉树的节点按照从上至下、从左至右的顺序进行编号,如果编号为 i 的节点与满二叉树中编号为 i 的节点在二叉树中的位置相同,则这颗二叉树称为完全二叉树。
完全二叉树特点
- 叶子节点只能出现在最下层、次下层。
- 最下层的叶子节点集中在树的左部。
堆排序是一种树形选择排序方法,它的特点是在排序过程中,将L[1...n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲节点和孩子节点之间的内在关系,在当前无序区中选择关键字大(或最小)的元素。
堆的定义
n 个关键字序列 L[1...n] 称为堆,当且仅当该序列满足:
- L(i) <= L(2i) 且 L(i) <= L(2i+1)
- 或者L(i) >= L(2i) 且 L(i) >= L(2i+1)
满足第一种情况的堆称为小根堆,满足第二种情况的称为大根堆
堆排序的基本思想
- 将整个待排记录分为有序区和无序区,初始时有序区为空,无序区为[L1,L2....Ln]。
- 构造初始堆。将初始无序区中记录看做一个顺序存放的完全二叉树上的节点,对该完全二叉树按照堆定义进行调整,使得关键字最大的记录成为二叉树的根节点,即L1
- 每趟排序操作。将根节点记录与无序区的最后一个节点交换,并将无序区的最后一个记录划入有序区内。
- 堆调整。由于交换后新的根L1可能违反了堆的定义,故应将当前无序区重新调整为堆,使得根节点的关键字最大。
- 重复3、4步骤,直到无序区为空位置
注意
- 小根堆和大根堆类似,小根堆得到的是逆序,大根堆得到的是正序。
- 堆排序和直接选择排序相反,在任何时刻,堆排序的无序区总在有序区之前,且有序区是在无序区的尾部向前逐步扩大至整个序列。
堆调整算法
每趟排序之前是以L1为根的堆,在L1与Ln交换之后,新的无序区中只有L1的值发生了变化,所以除了L1违反了堆的性质外,其余以任何节点为根的子树都是堆。因此需要对当前根节点按照堆性质进行调整,使得调整后得到新的堆。
- 每趟排序在完成堆顶元素与堆中最后一个元素交换后,将当前根节点的值与左、右子树(若存在)的根节点值进行比较,若当前根节点的值不小于这两个孩子节点的值,则当前根节点未违反堆的定义,无序调整。否则将当前根节点和它的两个孩子中较大者进行交换。
- 重复上述步骤,直到当前被调整的节点已满足堆的定义,或者孩子节点已经是叶子节点为止。
时间复杂度为O(n*log2*n),n乘以log 2底n,空间复杂度为O(1)。
def heap_sort(target):
def _init():
if not target:
return target
# 列表的第0个元素无效,数据从target[1]开始。
target.insert(0, 0)
def _heap_rectify(low, high):
tmp = target[low]
i = low
j = 2 * i
while j <= high:
if j < high and target[j] < target[j+1]:
j += 1
if tmp < target[j]:
target[i] = target[j]
i = j
j = 2 * i
else:
break
target[i] = tmp
def _heap_build():
length = len(target) - 1
for i in range(length // 2, 0, -1):
_heap_rectify(i, length)
def _heap_sort():
length = len(target) - 1
for i in range(length, 1, -1):
tmp = target[i]
target[i] = target[1]
target[1] = tmp
_heap_rectify(1, i-1)
_init()
_heap_build()
_heap_sort()
target.pop(0)
return target
print(heap_sort([4, 1, 0, -1, 9]))
_heap_rectify
是堆调整函数, _heap_build
堆初始化函数,_heap_sort
堆排序。列表的第0个元素无效,数据从 target[1] 开始。
四. 归并排序
归并排序是将若干个有序的子序列进行归并,从而合成一个新的有序的序列,其方法是:比较各子序列的第一个记录的关键字,最小的一个就是排序后序列的第一个记录关键字,取出这个关键字,继续比较各子序列当前的第一个记录的关键字,便可以找出排序后的第二个记录,如此下去,知道各子序列均为空为时便得到了最终的排序结果。
二路归并排序
如果同时利用两个有序序列的合并来实现归并排序,这种归并排序称为二路归并排序,二路归并排序是最简单的归并算法。
二路归并排序循环实现
时间复杂度为O(n*log2*n),n乘以log 2底n,空间复杂度为O(n)。
def merge_sort(target):
length = len(target)
target.insert(0, 0)
def _merge(low, middle, high):
i, j, k = low, middle+1, 0
ret = []
while i <= middle and j <= high:
if target[i] < target[j]:
ret.append(target[i])
i, k = i+1, k+1
else:
ret.append(target[j])
j, k = j+1, k+1
while i <= middle:
ret.append(target[i])
i, k = i+1, k+1
while j <= high:
ret.append(target[j])
j, k = j+1, k+1
i, k = low, 0
while i <= high:
target[i] = ret[k]
i, k = i+1, k+1
del ret
def _merge_pass(sub_len):
i = 1
while i+2*sub_len-1 <= length:
_merge(i, i+sub_len-1, i+2*sub_len-1)
i = i + 2*sub_len
if i+sub_len-1 < length:
_merge(i, i+sub_len-1, length)
def _merge_sort():
i = 1
while i < length:
_merge_pass(i)
i = 2 * i
_merge_sort()
target.pop(0)
return target
print(merge_sort([-1, -1, 0, 9, -1]))
二路归并排序递归实现
def merge_sort(target):
def _merge(low, middle, high):
i, j, k = low, middle+1, 0
ret = []
while i <= middle and j <= high:
if target[i] < target[j]:
ret.append(target[i])
i, k = i+1, k+1
else:
ret.append(target[j])
j, k = j+1, k+1
while i <= middle:
ret.append(target[i])
i, k = i+1, k+1
while j <= high:
ret.append(target[j])
j, k = j+1, k+1
i, k = low, 0
while i <= high:
target[i] = ret[k]
i, k = i+1, k+1
del ret
def _merge_sort(low, high):
if low < high:
middle = (low + high) // 2
_merge_sort(low, middle)
_merge_sort(middle+1, high)
_merge(low, middle, high)
_merge_sort(0, len(target)-1)
return target
print(merge_sort([-1, -1, 0, 9, -1]))
五. 基数排序
基数排序与前面的排序算法思想有很大不同,前面的排序算法都是以关键字的比较和数据的移动为主,但是基数排序却主要是通过“分配”和“收集”两种操作来实现目的的,是用多对关键字进行排序的思想实现对单个关键字进行排序的方法。
基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法。
它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较,将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
- 时间复杂度为O(d(n+rd))。 d表示关键字的位数,n表示待排序元素个数,rd表示基数(比如0~9)
- 空间复杂度为O(n+2rd)。n表示待排序元素个数,rd表示基数(比如0~9)
class RadixSort:
key_size = 5 # 表示排序数字的最大位数
radix = 10 # 基数为10(0,1,2,3,4,5,6,7,8,9)个
head = [0] * radix # 队列的队头
tail = [0] * radix # 队列的队尾
class Node:
__slots__ = ('value', 'next', 'key')
def __init__(self, value, next_one=0):
key_size = RadixSort.key_size
self.value = value
self.next = next_one
self.key = [int(i) for i in str(abs(self.value))[:key_size]]
while len(self.key) != key_size:
self.key.insert(0, 0)
def __str__(self):
return f'value:{self.value}, next:{self.next}, key:{self.key}'
@classmethod
def _distribute(cls, nodes, position):
head, tail = cls.head, cls.tail
for j in range(cls.radix):
head[j] = tail[j] = 0
p = nodes[0].next
while p != 0:
j = nodes[p].key[position]
if head[j] == 0:
head[j] = p
else:
t = tail[j]
nodes[t].next = p
tail[j] = p
p = nodes[p].next
@classmethod
def _collect(cls, nodes):
head, tail = cls.head, cls.tail
j = 0
while head[j] == 0:
j += 1
nodes[0].next = head[j]
t = tail[j]
while j < cls.radix-1:
j += 1
while j < cls.radix-1 and head[j] == 0:
j += 1
if head[j] != 0:
nodes[t].next = head[j]
t = tail[j]
nodes[t].next = 0
@classmethod
def sort(cls, target):
length = len(target)
target.insert(0, 0)
nodes = []
for i in range(length+1):
node = cls.Node(target[i], i+1)
nodes.append(node)
nodes[-1].next = 0
position = cls.key_size - 1
while position >= 0:
cls._distribute(nodes, position)
cls._collect(nodes)
position -= 1
ret = []
current = 0
while True:
current = nodes[current].next
if current == 0:
break
node = nodes[current]
ret.append(node.value)
return ret
print(RadixSort.sort([544, 3, 19, 324]))