vlambda博客
学习文章列表

经典 | Python实现八大排序算法,面试必备

作者:SkecisAI    来源:
https://skecis.top/2019/11/22/python排序/

序言

本文使用python实现了一些常用的排序方法。文章结构如下如下:

  1. 直接插入排序

  2. 希尔排序

  3. 冒泡排序

  4. 快速排序

  5. 简单选择排序

  6. 堆排序

  7. 归并排序

  8. 基数排序


上述所有的排序均写在一个python自定义类中,作为成员函数。

排序方法详细介绍

直接插入排序

直接插入排序(Straight Insertion Sort)是一种最简单的排序方法,它的基本操作是一个值插入到已排好序的有序表中,从而得到一个新的、记录数增1的有序表。如下图所示:

由上图可知若最初始的有序表即为数组的第一个元素。用python实现如下:

def straight_insertion_sort(self, value_list):
"""
直接插入排序
:param value_list: 无序列表
:return:
"""

return self.__straight_insert(value_list)

def __straight_insert(self, value_list):
sorted_list = []
sorted_list.append(value_list.pop(0))
for i in range(0, len(value_list)):
tail = True # 是否在尾部插入
insert_loc = 0
for j in range(len(sorted_list)):
if value_list[i] <= sorted_list[j]:
tail = False
insert_loc = j
break
sorted_list.append(value_list[i]) # 先将值插入尾部
if not tail:
# 移动值
for j in range(len(sorted_list) - 1, insert_loc, -1):
tmp = sorted_list[j]
sorted_list[j] = sorted_list[j - 1]
sorted_list[j - 1] = tmp
return sorted_list

希尔排序

希尔排序(Shell’s Sort)又称“缩小增量排序”(Diminishing Incerement Sort),它也是一种数插入排序的方法,但在时间效率上较前面的排序方法有较大的改进。它的基本思想是:先将整个待排记录序列分割成若干个子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。如下图所示:

经典 | Python实现八大排序算法,面试必备

即根据增量将原序列分割成多个子序列进行直接插入排序。增量应不断减小,且最后一个增量为1。用python实现如下(其中用到的子函数见前文):

def shells_sort(self, value_list):
"""
希尔排序
:param value_list: 无序列表
:return:
"""

gap = len(value_list) // 2
while gap >= 1:
i = 0
while (i + gap) < len(value_list):
start = i
gap_list = []
while start < len(value_list):
gap_list.append(value_list[start])
start = start + gap
gap_list = self.__straight_insert(gap_list)
start = i
while start < len(value_list):
value_list[start] = gap_list.pop(0)
start = start + gap
i += 1
gap = gap // 2
sorted_list = value_list
return sorted_list

冒泡排序

冒泡排序的过程很简单。首先将第一个记录的关键字和第二个记录的关键字进行比较,若逆序(与需要的顺序相反),则将两个记录交换之,然后比较第二个记录和第三个记录的关键字,以此类推。为第一趟冒泡结束,接着对前 n 1 个记录继续进行上述的过程。这样重复的过程直至 n 1 = 1 结束。排序过程如下所示:

经典 | Python实现八大排序算法,面试必备

用python实现如下:

def bubule_sort(self, value_list):
"""
冒泡排序
:param value_list: 无序列表
:return:
"""

for i in range(len(value_list) - 1):
for j in range(i + 1, len(value_list)):
if value_list[i] > value_list[j]:
tmp = value_list[j]
value_list[j] = value_list[i]
value_list[i] = tmp
sorted_list = value_list
return sorted_list

快速排序

快速排序(Quick Sort)是对冒泡排序的的一种改进。它的基本思想是,通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,已达到整个序列有序。其排序思想如下:

首先任意选取一个记录(通常可选第一个记录)作为枢轴,然后按下述原则重新排列记录:将所有关键字较它小的记录都安置在它的位置之前,将所有关键字较它大的记录都安置在它的位置之后。一趟快速排序的具体做法是:设两个指针low和high,他们的初值分别为最低位置的下一个位置和最高位,设最低位置位枢轴的关键字为pivotkey,则首先从high所指位置起像前搜索找到第一个关键字小于pivotkey的记录和枢轴记录互相交换。发生了交换后才从low所指向的位置起向后搜索,找到第一个关键字大于pivotkey的记录和枢轴记录互相交换。重复这两步直至low=how为止

如下图所示:

经典 | Python实现八大排序算法,面试必备

特别要注意换方向的时机是发生了交换后,用python实现如下:


def quick_sort(self, value_list):
"""
快速排序
:param value_list: 无序列表
:return:
"""

low = 0
high = len(value_list) - 1
self.__qsort(value_list, low, high)
sorted_list = value_list
return sorted_list

def __qsort(self, val_list, low, high):
"""
快速排序辅助函数
:param val_list: 无序列表
:param low: 低位
:param high: 高位
:return:
"""

if low >= high:
return
pivot_key = low
tmp_low = pivot_key
tmp_high = high
while low < high: # 分成一边比轴(pivot)大,一边比轴(pivot)小的顺序
while low < high:
if val_list[high] < val_list[pivot_key]:
tmp = val_list[high]
val_list[high] = val_list[pivot_key]
val_list[pivot_key] = tmp
pivot_key = high
break # 发生交换后,就换方向
else:
high -= 1
while low < high:
if val_list[low] > val_list[pivot_key]:
tmp = val_list[low]
val_list[low] = val_list[pivot_key]
val_list[pivot_key] = tmp
pivot_key = low
break # 发生交换后,就换方向
else:
low += 1
self.__qsort(val_list, tmp_low, pivot_key - 1)
self.__qsort(val_list, pivot_key + 1, tmp_high)

简单选择排序

选择排序的基本思想是:每一趟在n−i+1(i=1,2,…,n−1)个记录中选取关键字最小的记录作为有序序列中第i个记录。简单选择排序:通过n−1次关键字的比较,从n−i+1个记录中选出关键字最小的记录,并和第i(1≤i≤n)个记录交换之。如下图所示:

经典 | Python实现八大排序算法,面试必备

用python实现如下:

def simple_selection_sort(self, value_list):
"""
简单选择排序
:param value_list: 无序列表
:return:
"""

for i in range(len(value_list)):
min_val = 9999999
for j in range(i, len(value_list)):
if min_val > value_list[j]:
min_val = value_list[j]
count = 0 # 如果有多个相同的最小值
for j in range(i, len(value_list)):
if min_val == value_list[j]:
tmp = value_list[j]
value_list[j] = value_list[i + count]
value_list[i + count] = tmp
count += 1
sorted_list = value_list
return sorted_list

堆排序

的定义如下: n 个元素的序列 { k 1 , k 2 , k n } 当且仅当满足一下关系时,称之为堆。

经典 | Python实现八大排序算法,面试必备


若将序列看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端节点均不大于(或不小于)其左、右孩子节点的值。由此,若序列是堆,则堆顶元素必为序列中的最小值(或最大值)如下图所示:


经典 | Python实现八大排序算法,面试必备

至此,我们可以给出堆排序的过程:若在输出堆顶的最小值后,使得剩余 n 1 个元素的序列又建成一个堆,则得到 n 个元素中的次小值。如此反复执行,便能得到一个有序序列。
故整个堆排序可以大致分为两个过程:

  • 将无序序列建成堆。

  • 输出堆顶元素后,用类似建堆的方法调整堆。


如下两个图所示:


经典 | Python实现八大排序算法,面试必备


经典 | Python实现八大排序算法,面试必备

根据堆排序的特点总结出两点注意事项:

  1. 利用把堆看成完全二叉树的特点,用完全二叉树的性质解决算法问题

  2. 建堆的过程是从树种的最后一个非终端节点逆序开始调整的。

  3. 每调整一次需要检查前后是否依然保持堆的特征

本文利用了二叉树的孩子兄弟表示法来生成二叉树(堆)的。代码如下:

class __CldSibNode:
"""
私有内部类:孩子兄弟二叉链表节点
"""


def __init__(self, val):
self.value = val
self.child = None
self.sibling = None

def heap_sort(self, value_list):
"""
堆排序
:param value_list: 无序列表
:return:
"""

sorted_list = []
root_node = self.__CldSibNode(None)
self.__child_sibling(root_node, value_list, 0)
for ct in range(1, len(value_list) // 2 + 1): # 建堆
self.__adjust_heap(root_node, len(value_list) // 2 + 1 - ct, 1)
for i in range(1, len(value_list) + 1): # 堆排序
sorted_list.append(root_node.value) # 输出堆顶元素
head = root_node
self.__shrink_heap(root_node, len(value_list) + 1 - i, 1, head) # 收缩堆
self.__adjust_heap(root_node, 1, 1) # 调整堆
return sorted_list

def __child_sibling(self, node, value_list, ind):
"""
创建完全二叉树的左孩子右兄弟二叉链表
:param node: 当前节点
:param value_list:
:return:
"""

if ind >= len(value_list):
return
node.value = value_list[ind]
if (ind * 2 + 1) < len(value_list):
node.child = self.__CldSibNode(None) # 孩子
self.__child_sibling(node.child, value_list, ind * 2 + 1)
if (ind * 2 + 2) < len(value_list):
node.child.sibling = self.__CldSibNode(None) # 兄弟
self.__child_sibling(node.child.sibling, value_list, ind * 2 + 2)

def __adjust_heap(self, root_node, last_ind, now_ind):

def change(x, y):
"""
内部函数:交换两个变量值
"""

return y, x

if (not root_node) or (not root_node.child): # 不为空且有孩子
return
if now_ind == last_ind: # 需要调整的非终端节点
tmp = root_node
cg = False
while tmp.child:
if tmp.value > tmp.child.value: # 如果大于左子树根节点
tmp.value, tmp.child.value = change(tmp.value, tmp.child.value)
cg = True # 发生交换
if tmp.child.sibling:
if tmp.value > tmp.child.sibling.value:
if cg: # 如果发生过交换
tmp.value, tmp.child.value = change(tmp.value, tmp.child.value)
tmp.value, tmp.child.sibling.value = change(tmp.value, tmp.child.sibling.value)
tmp = tmp.child.sibling
continue
else:
if cg: # 如果发生过交换
tmp = tmp.child
continue
break
# 递归
self.__adjust_heap(root_node.child, last_ind, now_ind * 2)
if root_node.child.sibling:
self.__adjust_heap(root_node.child.sibling, last_ind, now_ind * 2 + 1)

def __shrink_heap(self, root_node, last_ind, now_ind, head):

if (not root_node) or (now_ind * 2 > last_ind): # 不为空
return
if last_ind == (now_ind * 2 + 1):
head.value = root_node.child.sibling.value
root_node.child.sibling = None
return True
if last_ind == (now_ind * 2):
head.value = root_node.child.value
root_node.child = None
return True
if root_node.child:
self.__shrink_heap(root_node.child, last_ind, now_ind * 2, head)
self.__shrink_heap(root_node.child.sibling, last_ind, now_ind * 2 + 1, head)

归并排序

归并排序(Merging),“归并”的含义是将两个或两个以上的有序表组合成一个新的有序表。假设初始序列有 n 个记录,则可看成是 n 个有序的子序列,每个子序列的长度为1,然后两两归并,得到 n/ 2 个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为 n 的有序序列为止,这种排序方法称为2-路归并排序。算法的基本思想如下图所示:

经典 | Python实现八大排序算法,面试必备

其中两个子序列的合并大有学问,基本思想就是:分别在两个序列头设置指针,比较两个序列指针所指的值的大小,将满足要求的值提取出来形成新列表,并将指针右移。当其中一个指针指向结尾之后时,表示其中一个列表已取尽,接着直接在新列表尾部连接另一个列表。如下图所示:

经典 | Python实现八大排序算法,面试必备

用python实现如下:

def merging_sort(self, value_list):
"""
归并排序
:param value_list: 无序列表
:return:
"""

i = 0
while np.power(2, i) < len(value_list):
count = np.power(2, i)
start = 0
outer_tmp = []
while start < len(value_list):
other = start + count # 定位另一边
tmp = []
if other >= len(value_list): # 另一边不存在
outer_tmp.extend(value_list[start:start + count]) # 直接合并
break # 结束
left, right = 0, 0
while (left < count) or (right < count):
if other + right >= len(value_list): # 右边提前结束
tmp.extend(value_list[start + left:start + count])
break
elif value_list[start + left] < value_list[other + right]: # 左边更小
tmp.append(value_list[start + left])
left += 1
if left == count: # 左边遍历结束
tmp.extend(value_list[other + right:other + count]) # 合并右边
break
else: # 右边更小
tmp.append(value_list[other + right])
right += 1
if right == count: # 右边遍历结束
tmp.extend(value_list[start + left:start + count]) # 合并左边
break
outer_tmp.extend(tmp)
start += count * 2
value_list = outer_tmp
i += 1
sorted_list = value_list
return sorted_list

基数排序

基数排序(Radix Sort)是利用了多关键字排序的思想,分为从低位关键字开始排序的最低位优先法(Least Significant Digit first)和从高位关键字开始排序的高位优先法(Most Signficant Digit first)。基数排序是借助“分配”和“收集”两种操作对单逻辑关键字进行排序的一种内部排序方法。

如对三位数(0~999,即有三个关键字)进行最低位优先法排序:首先以静态链表存储 n 个待排记录,并另表头指针指向第一个记录;第一趟分配对低位关键字个位数进行,初始化10个空队列,每个队列中记录关键字的个位数相等。第一趟收集是改变所有费控队列的队尾指针域,令其指向下一个非空队列的队头,重新将10个队列链接成一个链表。第二次、第三次分别对十位和百位进行同样的操作,直至排序结束。


如下图所示:


经典 | Python实现八大排序算法,面试必备



排序时有两点需要注意:

  1. 每完成一趟排序,要清空队列。

  2. 队列的连接要找到第一个不为空的队列作为头,和绕开所有空队列。


用python实现如下:

def radix_sort(self, value_list):
"""
基数排序
:param value_list: 无序列表
:return:
"""


def get_n(num, n):
"""
内部函数:得到数的第n位
"""

remainder = num % np.power(10, n)
quotient = remainder // np.power(10, n - 1)
return quotient

max_val = -99999
for val in value_list:
if val > max_val:
max_val = val
iter_nums = 0 # 最大位数
while max_val > 0:
iter_nums += 1
max_val = max_val // 10
for bit in range(1, iter_nums + 1): # 开始排序
tmp_list = []
qs = [] # 创建新的链式队列数组
for i in range(10):
qs.append(que.LinkedQueue())
for val in value_list:
qs[get_n(val, bit)].enqueue(val) # 按位数入队
qhead = None
tmp_q = None
for j in range(10):
if qs[j].front and (qhead is None):
qhead = qs[j] # 寻找重组队列头
tmp_q = qhead
continue
elif qs[j].front:
tmp_q.rear.pnext = qs[j].front # 将队列按顺序连接
tmp_q = qs[j]
tmp = qhead.front # 链接队列头
while tmp: # 更新数组列表
tmp_list.append(tmp.value)
tmp = tmp.pnext
value_list = tmp_list
sorted_list = value_list
return sorted_list

测试代码

编写测试代码和运行结果如下:

if __name__ == '__main__':
# 排序之道
li = list(np.random.randint(1, 1000, 30))
my_sort = MySort()
print('original sequence: ', li)
print('-' * 170)
print('1.', my_sort.straight_insertion_sort.__name__, ':', my_sort.straight_insertion_sort(li.copy()))
print('2.', my_sort.shells_sort.__name__, ': ', my_sort.shells_sort(li.copy()))
print('3.', my_sort.bubule_sort.__name__, ': ', my_sort.bubule_sort(li.copy()))
print('4.', my_sort.quick_sort.__name__, ': ', my_sort.quick_sort(li.copy()))
print('5.', my_sort.simple_selection_sort.__name__, ': ', my_sort.simple_selection_sort(li.copy()))
print('6.', my_sort.heap_sort.__name__, ': ', my_sort.heap_sort(li.copy()))
print('7.', my_sort.merging_sort.__name__, ': ', my_sort.merging_sort(li.copy()))
print('8.', my_sort.radix_sort.__name__, ': ', my_sort.radix_sort(li.copy()))

测试运行结果:

original sequence:  [758, 74, 857, 781, 831, 719, 489, 785, 405, 621, 165, 568, 915, 939, 886, 4, 966, 461, 385, 757, 263, 505, 793, 259, 107, 437, 296, 702, 240, 644]
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1. straight_insertion_sort : [4, 74, 107, 165, 240, 259, 263, 296, 385, 405, 437, 461, 489, 505, 568, 621, 644, 702, 719, 757, 758, 781, 785, 793, 831, 857, 886, 915, 939, 966]
2. shells_sort : [4, 74, 107, 165, 240, 259, 263, 296, 385, 405, 437, 461, 489, 505, 568, 621, 644, 702, 719, 757, 758, 781, 785, 793, 831, 857, 886, 915, 939, 966]
3. bubule_sort : [4, 74, 107, 165, 240, 259, 263, 296, 385, 405, 437, 461, 489, 505, 568, 621, 644, 702, 719, 757, 758, 781, 785, 793, 831, 857, 886, 915, 939, 966]
4. quick_sort : [4, 74, 107, 165, 240, 259, 263, 296, 385, 405, 437, 461, 489, 505, 568, 621, 644, 702, 719, 757, 758, 781, 785, 793, 831, 857, 886, 915, 939, 966]
5. simple_selection_sort : [4, 74, 107, 165, 240, 259, 263, 296, 385, 405, 437, 461, 489, 505, 568, 621, 644, 702, 719, 757, 758, 781, 785, 793, 831, 857, 886, 915, 939, 966]
6. heap_sort : [4, 74, 107, 165, 240, 259, 263, 296, 385, 405, 437, 461, 489, 505, 568, 621, 644, 702, 719, 757, 758, 781, 785, 793, 831, 857, 886, 915, 939, 966]
7. merging_sort : [4, 74, 107, 165, 240, 259, 263, 296, 385, 405, 437, 461, 489, 505, 568, 621, 644, 702, 719, 757, 758, 781, 785, 793, 831, 857, 886, 915, 939, 966]
8. radix_sort : [4, 74, 107, 165, 240, 259, 263, 296, 385, 405, 437, 461, 489, 505, 568, 621, 644, 702, 719, 757, 758, 781, 785, 793, 831, 857, 886, 915, 939, 966]

总结

各个排序效率见下图:

经典 | Python实现八大排序算法,面试必备


可以得出以下几个结论:

  1. 从平均时间性能而言,快速排序最佳。

  2. 堆排序适用于 n 较大的数据。

  3. 基数排序是稳定的,时间复杂度较大的简单排序方法也是稳定的。

  4. 稳定性是由方法本身决定的。

  5. 没有最好的排序方法,视情况而定。

源码链接

https://github.com/SkecisAI/python-sort/blob/master/my_sort.py


(完)


看完本文有收获?请转发分享给更多人
关注「Python那些事」,做全栈开发工程师
点「在看」的人都变好看了哦