在文章【数据结构Python描述】列表实现原理深入探究及其常用操作时间复杂度分析中,我们通过分析列表 list
的实现原理,并基于连续存储的内存模型实现了一个类似 list
的类,其中支持的一个重要方法是 insert
,而使用和该方法类似的策略,我们可以实现排序算法中的一类——插入排序。
一、插入排序简介
1. 算法原理
对于插入排序,可使用下图所示的流程来说明,初始有一个无序的序列['B', 'C', 'D', 'A', 'E', 'H', 'G', 'F']
,其中的元素(实际上是元素引用,原因请见【数据结构Python描述】字符串、元组、列表内存模型简介)存储在连续内存中,通过下列步骤可以将其按照字母表顺序进行排序:
- 取出元素
'B'
,由于一个元素无所谓的顺序,故直接将其插入索引为0处; - 取出元素
'C'
,按字母表顺序,其就应该直接紧随'B'
插入索引为1处; - 取出元素
'D'
,同上; - 取出元素
'A'
,由于其需要排在'B'
,'C'
,'D'
之前,故需要先依次将这三个元素向右移一个单元,然后在索引为0处插入'A'
; - 取出元素
'E'
,同前三个元素; - 取出元素
'H'
,同上; - 取出元素
'G'
,先将元素'H'
向右移动一个单元,然后在索引为5处插入'G'
; - 取出元素
'F'
,进行和插入元素'A'
时的类似流程。
下列是插入排序的算法伪代码:
Algorithm InsertSort(A):
# 输入:具有n个可比较元素的序列
# 输出:具有n个元素的有序序列
for k from 1 to n - 1 do:
# 将元素A[k]插入A[0],A[1],...,A[k]中某一个位置
2. 算法实现
下面是使用 Python 实现的插入排序代码,其中使用两层循环结构:
- 外层循环用于依次取出每一个待排列的元素;
- 内层循环用于将每一个取出的待排列元素和其左边已排好子序列元素进行比较。
def insertion_sort(A):
"""将保存可比较元素的列表排列为元素单调递增的序列"""
for k in range(1, len(A)): # 从第2个元素开始排序
cur = A[k]
j = k
while j > 0 and A[j - 1] > cur:
A[j] = A[j - 1]
j -= 1
A[j] = cur # 将元素cur插入正确的位置
3. 算法分析
因为嵌套循环的缘故,插入排序的算法在最坏情况下(即初始序列为单调递减序列)时间复杂度为 O ( n 2 ) O(n^2) O(n2) ,在最好情况下(即初始序列已经为单调递增序列)该算法的时间复杂度为 O ( n ) O(n) O(n) 。
二、插入排序应用
1. 需求简介
假设现在有这样一个需求:设计一个游戏的高分榜的功能,使其可以按照分数从高到低排列指定数量的游戏记录。
2. 需求实现
首先需要定义记录一条游戏记录的类,为简单起见,这里定义一个GameEntry
类,其中有两个私有属性:
_name
:该条游戏记录的创建者;_score
:该条游戏记录的分数。
其次定义一个高分榜类ScoreBoard
,这里需要注意的是,因此要求该高分榜只能存储指定数量的游戏记录,因而当高分榜中的游戏记录条目数已经等于指定的最大条目数,此时只有当新的记录大于现有高分榜中最低分时,该条记录才可以进入高分榜。
在ScoreBoard
类的内部,我们使用两个实例属性:
_board
:一个Python列表,用来保存所有GameEntry
的实例对象,由于高分榜所能保存的游戏记录条目数有限,因此我们将列表_board
初始化为最大条目数个None
,这样就不需要动态地扩增容量;_n
:记录当前_board
中已保存的高分游戏记录条目数。
下面是实现上述需求的全部代码:
class GameEntry:
"""用于记录高分榜的类"""
def __init__(self, name, score):
self._name = name
self._score = score
@property
def name(self):
return self._name
@property
def score(self):
return self._score
def __str__(self):
return '({0}, {1})'.format(self._name, self._score)
class ScoreBoard:
"""以非降序排列的具有固定长度序列,存储固定游戏记录的高分榜"""
def __init__(self, capacity=10):
"""初始化具有指定最大容量的高分榜,所有元素初始为None"""
self._board = [None] * capacity
self._n = 0
def __getitem__(self, k):
"""返回索引为k的位置的游戏记录"""
return self._board[k]
def __str__(self):
"""返回记录高分榜的字符串表示形式"""
return '\n'.join(str(self._board[j]) for j in range(self._n))
def add(self, entry: GameEntry):
"""尝试将新的分数记录插入高分榜榜中"""
score = entry.score
# 判断新的游戏记录是否能够进入高分榜榜:是否高分榜未满或该记录分数大于高分榜上最低分
if self._n < len(self._board) or score > self._board[-1].score:
if self._n < len(self._board): # 高分榜未满
self._n += 1
# 所有游戏记录向右移动一个单元
j = self._n - 1
while j > 0 and self._board[j - 1].score < score:
self._board[j] = self._board[j - 1]
j -= 1
self._board[j] = entry # 在空出的单元插入新的游戏记录
def main():
mike = GameEntry('Mike', 1105)
bob = GameEntry('Bob', 750)
paul = GameEntry('Paul', 720)
anna = GameEntry('Anna', 660)
rose = GameEntry('Rose', 590)
jack = GameEntry('Jack', 510)
board = ScoreBoard()
board.add(mike)
board.add(bob)
board.add(paul)
board.add(anna)
board.add(rose)
board.add(jack)
jill = GameEntry('Jill', 740)
board.add(jill)
print(board)
if __name__ == '__main__':
main()
上述代码中还实现了__getitem__
和__str__
两个魔法方法:
__getitem__
:使得可以使用board[i]
这样的语法来访问ScoreBoard
实例对象中的一条游戏记录;__str__
:返回一个ScoreBoard
实例对象的字符串表示形式。
实际上,上述代码中最重要的方法是用于向高分榜中添加游戏记录的add
方法,我们只要记着新的游戏记录可以插入高分榜中的条件是:高分榜未满或新的游戏记录分数大于已有记录中的最低分,其他的代码就是对于插入排序的类似实现。