选择排序法就是反复从未排序的数列中取出最小(或最大)的数据,将其存放在序列的起始位置,然后,再从未排序的元素中继续寻找最小(或最大)的数据,存放在已排序序列的末尾,以此类推。最后的结果即为已排序好的数列。选择排序法最后的结果有两种形式,即递增数列和递减数列,下面就对于两种结果形式的具体排序流程进行描述。
接下来用一组数据来详细讲解选择排序法。例如,有这样一组数据:56,18,49,84,72,如下图所示:
按照递增顺序进行排序,步骤如下:
步骤1:找到所示数列中的最小值18与此数列中的第一个元素56交换,如下图所示:
步骤2:从第二个值开始,找到此数列中(不包含第一个值)的最小值49,再和第二个值 56 交换,如下图所示:
步骤3:从第三个值开始,找到此数列中(不包含第一、第二个值)的最小值56,由于它本来就是在第三个位置,故不需要进行交换。
步骤4:从第四个值开始,找到此数列中(不包含第一、第二、第三个值)的最小值72,再和第四个值84交换,如下图所示:
步骤5:数列递增顺序排序完毕,结果如下图所示:
【实例1】使用选择排序法进行递增排序。使用选择排序法为列表:56,18,49,84,72,进行递增排序。具体代码如下:
def choose(data, data_len): # 自定义一个选择排序法函数
for m in range(data_len - 1): # 遍历新数据
for n in range(m + 1, data_len):
if data[n] < data[m]: # 如果数据小于原来的数据
data[m], data[n] = data[n], data[m] # 需要交换位置
print('第 %d 次排序之后的结果是' % (m + 1), end='') # 提示
for n in range(data_len): # 遍历每次排序的结果
print('%3d' % data[n], end='') # 输出结果
print() # 输出空行
num_list = [56, 18, 49, 84, 72] # 创建数列并初始化
length = len(num_list)
print("原始数据为:") # 提示
for i in range(length): # 遍历原有数据
print('%3d' % num_list[i], end='') # 输出结果
print('\n---------------------------') # 输出分界符
choose(num_list, length) # 调用选择排序法函数
print('\n---------------------------') # 输出分界符
print("排序之后的数据为:") # 提示
for j in range(length): # 遍历排序好的新数列的数据
print('%3d' % num_list[j], end='') # 输出结果
print('') # 输出空行
冒泡排序法是观察水中气泡变化而创造的排序方法,它的基本原理是从第一个数据开始,比较相邻数据的大小,如果大小顺序有误,则对调之后再与下一个数据进行比较,就像气泡逐渐从水底上升到水面上的情况。经过这样不断交换之后,就可以找出最后一个数据的正确位置。接着逐步进行交换,直到完成所有数据的排序为止。
冒泡排序法最后的结果也有两种形式,即递增数列和递减数列。接下来用一组数据来详细讲解冒泡排序法的基本原理。
例如,有这样一组数据:56、20、84、66、13,如下图所示:
按照递增顺序进行排序,步骤如下:
步骤1:首先用第一个位置的数据 56 与第二个位置的数据 20 进行比较,因为 20 小于 56,所以进行交换;然后再用第二个位置的数据 56 与第三个位置的数据 84 进行比较,因为 56 小于 84,所以不用交换;再用第三个位置的数据 84 与第四个位置的数据 66 进行比较,因为 66 小于 84,所以进行交换;最后用第四个位置的数据 84 与第五个位置的数据 13 进行比较,因为 84 大于 13,所以进行交换。这样就完成了第一次的排序,排序过程如下图所示:
步骤2:经过第一次排序,已经将最大值 84 放在了对应的位置,因此在进行第二次排序时比较到 13 即可。
第二次排序依然从第一个位置开始比较,即比较数据 20 与 56 的大小,因为 20 小于 56,所以不需要交换;然后比较数据 56 与 66 的大小,因为 56 小于 66,所以不需要交换;最后比较数据 66 与 13 的大小,因为 66 大于 13,所以需要交换。这样就完成了第二次排序,排序过程如下图所示:
步骤3:经过第二次排序,已经将剩下数列 (除84以外) 的最大值 66 放在了对应的位置,因此在进行第三次排序时比较到 13 即可。
第三次排序依然从第一个位置开始进行比较,即比较数据 20 与 56 的大小,因为 20 小于 56,所以不需要交换位置;然后比较数据 56 与 13 的大小,因为 56 大于 13,所以需要交换位置。这样就完成了第三次排序,排序过程如下图所示:
步骤4:经过第三次排序,已经将数列 84,66,56 放在了对应的位置,因此在进行第四次排序时比较到 13 即可。
第四次排序依然从第一个位置开始比较,比较数据 20 与 13 进行的大小,因为 20 大于 13,所以需要交换位置。这样就完成了第四次排序,排序过程如下图所示:
至此,排序完成。
【实例2】使用冒泡排序法进行递增排序。使用冒泡排序法为列表:56,20,84,66,13,进行递增排序。具体代码如下:
def bubble(data, data_len): # 自定义一个冒泡排序法函数
traversal_times = data_len - 1
for m in range(traversal_times, 0, -1): # 遍历排序次数
for n in range(m): # 遍历新数据
if data[n + 1] < data[n]: # 如果数据小于原来的数据
data[n], data[n + 1] = data[n + 1], data[n] # 需要交换位置
print('第 %d 次排序之后的结果是' % (data_len - m), end='') # 提示
for n in range(data_len): # 遍历每次排序的结果
print('%3d' % data[n], end='') # 输出结果
print() # 输出空行
num_list = [56, 20, 84, 66, 13] # 创建数列并初始化
length = len(num_list)
print("原始数据为:") # 提示
for i in range(length): # 遍历原有数据
print('%3d' % num_list[i], end='') # 输出结果
print('\n---------------------------') # 输出分界符
bubble(num_list, length) # 调用冒泡排序法函数
print('---------------------------') # 输出分界符
print("排序之后的数据为:") # 提示
for i in range(length): # 遍历排序好的新数列的数据
print('%3d' % num_list[i], end='') # 输出结果
print('') # 输出空行
程序运行结果如下图所示:
从上图所示的运行结果来看,排序的步骤和上述介绍的冒泡排序法步骤完全吻合。
【练习1】黄金档各个电视台综艺收视率排名。如今各个电视台在每周五的黄金档会有独播的综艺节目,某周的电视台黄金档综艺的收视率情况数据如下:14,27,28,04,21(省略其%),用冒泡排序法把此收视率按照从高到低的顺序排序,程序运行结果如下图所示:
直接插入排序法是将数列中的数据,逐一与已排序好的数据进行比较。例如,在排好顺序的两个数据中插入第三个数据,就需要将其与排好的两个数据进行比较,通过比较结果将数据各自放在合适的位置。即在第三个数据插入数列时,这三个数据已然是排好顺序的。接着将第四个数据插入,以此类推,直到排序完成。
直接插入排序法最后的结果也有两种形式,即递增数列和递减数列。接下来通过一组数列具体来演示直接插入排序法的排序。例如,有这样一组数列:58,29,86,69,10,如下图所示:
步骤3:将第三个位置上的数据 86 与 29 和 58 进行比较,因为 86 大于 29 和 58,所以直接将 86 放在第三个位置上,如下图所示:
步骤4:将第四个位置上的数据 69 与 29、58 和 86 进行比较,因为 69 大于 29 和 58,且 69 小于 86,所以直接将 69 插入到 86 前面的位置上,将 86 向后移一位,如下图所示:
步骤5:将第五个位置上的数据 10 分别与 29,58,69 和 86 进行比较,因为 10 小于 29,58,69 和 86,所以直接将 10 插入到 29 前面的位置上,将 29,58,69,86 依次向后移一位,如下图所示:
直接插入排序最终的排序结果如下图所示:
至此,排序完成。
【实例3】使用直接插入排序法进行递增排序。使用直接插入排序法对列表:58,29,86,69,10 进行递增排序。具体代码如下:
def insert(data): # 自定义一个插入排序法函数
for i in range(5): # 遍历新数据
temp = data[i] # temp用来暂存数据
j = i - 1
# 循环排序,判断条件是数据的下标值要大于等于0且暂存数据小于原数据
while j >= 0 and temp < data[j]:
data[j + 1] = data[j] # 把所有元素往后移一位
j -= 1 # 下标减1
data[j + 1] = temp # 最小的数据插入最前一个位置
print('第 %d 次排序之后的结果是' % (i + 1), end='') # 提示
for j in range(5): # 遍历每次排序的结果
print('%3d' % data[j], end='') # 输出结果
print() # 输出空行
data = [58, 29, 86, 69, 10] # 创建数列并初始化
print("原始数据为:") # 提示
for i in range(5): # 遍历原有数据
print('%3d' % data[i], end='') # 输出结果
print('\n---------------------------') # 输出分界符
insert(data) # 调用直接插入排序法函数
print('\n---------------------------') # 输出分界符
print("排序之后的数据为:") # 提示
for i in range(5): # 遍历排序好的新数列的数据
print('%3d' % data[i], end='') # 输出结果
print('') # 输出空行
程序运行结果如下图所示:
从上图所示的结果看,结果与上述介绍的直接插入排序法步骤完全吻合。
【练习2】输出跳绳成绩排名。身体是革命的本钱,体育锻炼是一件非常重要的事。因此,中考内容就加入了体育考试,跳绳就是其中一项考试内容。例如,某校中考的五名考生跳绳个数如下:155,138,185,149,163,利用直接插入法给这五位考生从高到低顺序排序,具体代码如下:
def insert(data): # 自定义一个插入排序法函数
for i in range(5): # 遍历新数据
temp = data[i] # temp用来暂存数据
j = i - 1
# 循环排序,判断条件是数据的下标值要大于等于0且暂存数据大于原数据
while j >= 0 and temp > data[j]:
data[j + 1] = data[j] # 把所有元素往后移一位
j -= 1 # 下标减1
data[j + 1] = temp # 最小的数据插入最前一个位置
print('第 %d 次排序之后的结果是' % (i + 1), end='') # 提示
for j in range(5): # 遍历每次排序的结果
print('%6d' % data[j], end='') # 输出结果
print() # 输出空行
data = [155, 138, 185, 149, 163] # 创建数列并初始化
print("五名考生跳绳个数如下:") # 提示
for i in range(5): # 遍历原有数据
print('%6d' % data[i], end='') # 输出结果
print('\n---------------------------') # 输出分界符
insert(data) # 调用直接插入排序法函数
print('\n---------------------------') # 输出分界符
print("从高到低排序之后的跳绳个数如下:") # 提示
for i in range(5): # 遍历排序好的新数列的数据
print('%6d' % data[i], end='') # 输出结果
print('')
合并排序法是针对已经排序好的两个或两个以上的数列,通过合并的方式,将其组合成一个大的且排序好的数列。首先是将无序的数列分成若干小份,分若干份的规则就是 不断把每段长度除以2(对半分),直到分到不能再分为止,然后对分好的数列进行排序,最后再逐步合并成一个排序好的大数列,如下图所示:
合并排序法最后的结果也有两种形式,即递增数列和递减数列。接下来通过一组数据来演示合并排序法的排序原理。例如,有这样一组数据:33,10,49,78,57,96,66,21,如下图所示:
按照递增进行排序,步骤如下:
步骤1:将原始数列一分为二,得到两个数列,即数列1和数列2,数列1为33,10,49,78;数列2为57,96,66,21,如下图所示:
步骤2:再分别将上图所示的数列1和数列2分别一分为二,得到数列a、数列b、数列c和数列d。此时每个数列中包含两个数据,并将每份中的两个数据进行排序,如下图所示:
步骤3:将上图中排序好的数列元素进行合并,将数列a与数列b合并为数列A;将数列c与数列d合并为数列B。再对数列A和数列B中的数据元素进行排序,如下图所示:
步骤4:将上图中的数列A与数列B合并为一个数列,并将其内部的数据进行排序。最终的排序结果如下图所示:
至此,排序完成。
【实例4】使用合并排序法进行递增排序。使用合并排序法为列表:33,10,49,78,57,96,66,21进行排序。具体代码如下:
def merge_sort(data): # 自定义合并排序法函数
if len(data) <= 1: # 判断列表元素是否小于或等于1
return data # 当列表元素只有一个的时候,直接返回
mid = len(data) // 2 # 分隔长度计算,整个数据的长度除以2取整
left = data[:mid] # 左半边数据
right = data[mid:] # 右半边数据
left = merge_sort(left) # 调用merge_sort()函数继续对左半边分隔并排序
right = merge_sort(right) # 调用merge_sort()函数继续对右半边分隔并排序
# 递归地进行排序
result = [] # 用来存储结果值
while left and right: # 循环合并,判断条件是:左下标和右下标是否为真
if left[0] <= right[0]: # 判断左边数小于右边数
result.append(left.pop(0)) # 结果增加left[0]的值
else:
result.append(right.pop(0)) # 结果增加right[0]的值
if left: # 如果left的值为真
result += left # 结果显示左侧数据
if right: # 如果right的值为真
result += right # 结果显示右侧数据
return result # 返回排序后的结果
data = [33, 10, 49, 78, 57, 96, 66, 21] # 创建数列并初始化
print("原始数据为:", data) # 输出原始数据
print('------------------------------------------------------') # 输出分界符
print("排序之后的数据为:", merge_sort(data)) # 调用函数,输出排序好的数据
print('------------------------------------------------------') # 输出分界符
程序运行结果如下图所示:
【练习3】“双十一”品牌手机销量排名。“双十一”是每年的购物狂欢节,参与的商家不尽其数,当然品牌手机也是“双十一”的热门销售商品之一。假设某五个品牌手机销售量(单位是万台)分别是:2750,3429,1632,4019,3698。利用合并排序法给销售量按照从高到低的顺序排序,具体代码如下:
def merge_sort(data): # 自定义的合并排序法函数
if len(data) <= 1: # 判断列表元素是否小于或等于1
return data # 当列表元素只有一个的时候,直接返回
mid = len(data) // 2 # 分隔长度计算,整个数据的长度除以2取整
left = data[:mid] # 左半边数据
right = data[mid:] # 右半边数据
left = merge_sort(left) # 调用merge_sort()函数继续对左半边分隔并排序
right = merge_sort(right) # 调用merge_sort()函数继续对右半边分隔并排序
# 递归的进行排序
result = [] # 用来存储结果值
while left and right: # 循环合并,判断条件是:左下标和右下标是否为真
if left[0] >= right[0]: # 判断左边数大于右边数
result.append(left.pop(0)) # 结果增加left[0]的值
else:
result.append(right.pop(0)) # 结果增加right[0]的值
if left: # 如果left的值为真
result += left # 结果显示左侧数据
if right: # 如果right的值为真
result += right # 结果显示右侧数据
return result # 返回排序后的结果
data = [2750, 3429, 1632, 4019, 3698] # 创建数列并初始化
print("五大品牌手机销售量如下:", data) # 输出原始数据
print('================================') # 输出分界符
print("从高到低排序之后的销售量如下:", merge_sort(data)) # 调用函数,输出排序好的数据
print('================================') # 输出分界符
程序运行结果如下图所示:
希尔排序法是插入排序法的一种,是直接插入排序算法的更高级的改进版本。希尔排序法可以减少插入排序法中数据移动的次数,加快排序的进行,因此它被称为缩小增量排序。希尔排序法排序的原则是将原始数据分成特定间隔的几组数据,然后使用插入排序法对每组数据进行排序,排序之后,再减小间隔距离,然后重复插入排序法对每组数据排序,直到所有数据完成排序为止。希尔排序法最后的结果有两种形式,即递增数列和递减数列。接下来通过一组数据来演示希尔排序法的排序原理。
例如,一组原始数据:60,82,17,35,52,73,54,9,如下图所示:
按照递增进行排序,步骤如下:
步骤1:从上图中可以看出,原始值中有 8 个数据,将间隔位数设置为 8/2=4,即将原始值分为四组数列,分别为:数列1(60,52)、数列2(82,73)、数列3(17,54)、数列4(35,9),如下图所示。将每个数列内的数据进行排序,按照左小右大的原则,将位置错误的数据进行交换,即数列1(52,60)、数列2(73,82)、数列3(17,54)和数列4(9,35)。
说明:间隔位数不一定必须除以 2,可以根据自己的需求而定。
步骤2:将步骤1排序后的数列进行插入放置,得出第一次排序结果,如下图所示:
步骤3:接着缩小间隔[(8/2)/2=2],即将原数列分为两组数列,分别为:数列1(52,17,60,54)、数列2(82,35,73,9),如下图所示,然后再对上图中的每个数列内的数据进行排序,按照从小到大的顺序,将位置错误的数据进行交换,即数列1(17,52,54,60)和数列2(9,35,73,82)。
步骤4:将步骤3排序后的数列进行插入放置,得出第二次排序结果,如下图所示:
步骤5:再以[((8/2)/2)]2=1取间隔数,对第二次排序后的数列(如上图所示)中的每一个元素进行排序,得到最后的结果如下图所示:
至此,排序完成。
【实例5】使用希尔排序法进行递增排序。使用希尔排序法为列表:60,82,17,35,52,73,54,9进行递增排序。具体代码如下:
def hill(data): # 自定义希尔排序函数
n = len(data) # 获取数据长度
step = n // 2 # 让步长从大变小,最后一步必须是1,获取gap的偏移值
while step >= 1: # 只要gap在我们的合理范围内,就一直分组下去
# 按照步长把数据分两半,从步长的位置遍历后面所有的数据,指定j下标的取值范围
for j in range(step, n):
while j - step >= 0: # 拿当前位置的数据,跟当前位置-gap位置的数据进行比较
if data[j] < data[j - step]: # 组内大小元素进行替换操作
data[j], data[j - step] = data[j - step], data[j]
j -= step # 更新迁移元素的下标值为最新值
else: # 否则的话,不进行替换
break
step //= 2 # 每执行完毕一次分组内的插入排序,对gap进行/2细分
data = [60, 82, 17, 35, 52, 73, 54, 9] # 定义列表并初始化
print("原始数据:")
print(data) # 输出原数据
print("---------------------------------")
hill(data) # 调用自定义希尔排序函数
print("排序之后的数据:")
print(data) # 输出排序之后数据
print("---------------------------------")
程序运行结果如下图所示:
【练习4】热门游戏下载量排名。游戏是很多人消遣时间的一种方式,在手机的软件安装商店里,也有各类游戏。假设有6款游戏,它们的下载量(单位是万次)分别是:3009,2578,3044,9336,1111,5312。利用希尔排序法为下载量按照从高到低顺序进行排序。具体代码如下:
def hill(data): # 自定义希尔排序函数
n = len(data) # 获取数据长度
step = n // 2 # 让步长从大变小,最后一步必须是1,获取gap的偏移值
while step >= 1: # 只要gap在我们的合理范围内,就一直分组下去
# 按照步长把数据分两半,从步长的位置遍历后面所有的数据,指定j下标的取值范围
for j in range(step, n):
while j - step >= 0: # 拿当前位置的数据,跟当前位置-gap位置的数据进行比较
if data[j] > data[j - step]: # 组内大小元素进行替换操作
data[j], data[j - step] = data[j - step], data[j]
j -= step # 更新迁移元素的下标值为最新值
else: # 否则的话,不进行替换
break
step //= 2 # 每执行完毕一次分组内的插入排序,对gap进行/2细分
data = [3009, 2578, 3044, 9336, 1111, 5312] # 定义列表并初始化
print("六款游戏下载量如下:")
print(data) # 输出原数据
print("========================================")
hill(data) # 调用自定义希尔排序函数
print("从高到低排序之后的下载量如下:")
print(data) # 输出排序之后数据
print("========================================")
程序运行结果如下图所示:
快速排序法又称为分割交换法,它是冒泡排序法的一种改进。它的基本思想是:先在数据中找一个虚拟的中间值,并按此中间值将所有打算排序的数据分为两部分。其中,小于中间值的数据放在左边,大于中间值的数据放在右边,再用同样的方式处理左、右两边的数据,直到排序完成。操作的步骤如下:
假设有 n 项数据,数据值 用 K1,K2,…,Kn 来表示。
快速排序法最后的结果也有两种形式,即递增数列和递减数列。接下来通过一组数据来演示快速排序法的排序原理。例如,有这样一组数据:6,1,2,7,9,3,4,5,10,8,如下图所示:
按照递增进行排序,步骤如下:
步骤1:取原始值的第一个数 6 为虚拟中间值,即 K=6;然后从左向右查找值大于 6 的数,即数值 7,位置为 i,即 i=4;再从右向左查找值小于 6 的数,即数值 5,位置是 j,即 j=8,如下图所示:
步骤2:i
步骤3:i
步骤4:i>j,因此交换虚拟中间值K (数值6)和 Kj (数值3) 的位置,完成第三次排序。此时,发现 6 的左半边都是小于 6 的数,右半边都是大于 6 的数,虚拟中间值 6 变成了真正的中间值,如下图所示:
步骤5:对中间值 6 的左半边数据进行排序,中间值和右半边数据暂时可以忽略。在左半边取虚拟中间值 K=3,从左向右查找大于 3 的值,即数值 5,位置为 i,即 i=4;再从右向左查找小于 3 的值,即数值 2,位置为 j,即 j=3。如下图所示:
步骤6:i>j,因此需要交换 K (数值3) 和 Kj (数值2) 的值,如下图所示。此时虚拟中间值变成了真正的中间值。小于 3 的都在中间值 3 的左半边,大于 3 的都在中间值 3 的右半边。
步骤7:接下来对中间值为 3 的左、右两侧排序,经过排序之后,最终排序结果如下图所示:
步骤8:此时,整组数据的左半边已经完成排序,接下来对右半边进行排序,这次忽略已经排序好的左半边和中间值 6。取右半边第一个位置的数为虚拟中间值 K(数值9),然后从左向右找大于 9 的值,即数值 10,位置为 i,即 i=9;再从右向左找小于 9 的值,即数值 8,位置为 j,即 j=10。如下图所示:
步骤9:i
步骤10:i>j,因此交换 Kj (数值8) 和虚拟中间值 (数值9) 的值,此时,虚拟中间值变成为真正的中间值,如下图所示:
步骤11:以中间值为 9 的左右两侧进行排序,最后完成右半边排序,如下图所示:
结合左半边排序和右半边排序,最终的结果如下图所示:
至此,完成排序。
【实例6】使用快速排序法进行递增排序。使用快速排序法为列表:6,1,2,7,9,3,4,5,10,8,进行递增排序。具体代码如下:
def quick(data, start, end): # 定义快速排序法函数
if start > end: # 如果开始值大于结束值
return # 直接退出程序
i, j = start, end
result = data[start] # 取虚拟中间值
while True: # 循环
while j > i and data[j] >= result: # 从右向左找,找到的数比虚拟中间值小就停止循环
j = j - 1 # 从右向左找,位置每次-1
while i < j and data[i] <= result: # 从左向右找,找到的数比虚拟中间值大就停止循环
i += 1 # 从左向右找,位置每次+1
if i < j: # i和j都停止,找到对应的位置,判断i
data[i], data[j] = data[j], data[i] # 交换位置i和j对应的数值
elif i >= j: # 判断i>=j
# 交换虚拟中间值和j位置上的数,此时虚拟中间值变成真正中间值
data[start], data[j] = data[j], data[start]
break # 完成第一次排序,此时以中间值分左右两侧
quick(data, start, i - 1) # 调用快速排序函数,再快速排序左半边数据
quick(data, i + 1, end) # 调用快速排序函数,再快速排序右半边数据
data = [6, 1, 2, 7, 9, 3, 4, 5, 10, 8] # 定义列表并初始化
print("原始数据为:")
print(data) # 输出原始数据
print("--------------------------------")
quick(data, 0, (len(data) - 1)) # 调用快速排序,数据从位置0开始,到数据长度-1为止
print("排序之后的数据为:")
print(data) # 输出排序后数据
print("--------------------------------")
程序运行结果如图所示:
【练习4】入职年限排名。例如,某公司的6名职员入职年限分别是:1,3,15,20,5,4。利用快速排序法给这些职员入职年限按照从高到低顺序排序,具体代码如下:
def quick(data, start, end): # 定义快速排序法函数
if start > end: # 如果开始值大于结束值
return # 直接退出程序
i, j = start, end
result = data[start] # 取虚拟中间值
while True: # 循环
while j > i and data[j] <= result: # 从右向左找,找到的数虚拟中间值大就停止循环
j = j - 1 # 从右向左找,位置每次-1
while i < j and data[i] >= result: # 从左向右找,找到的数虚拟中间值小就停止循环
i += 1 # 从左向右找,位置每次+1
if i < j: # i和j都停止,找到对应的位置,判断i
data[i], data[j] = data[j], data[i] # 交换位置i和j对应的数值
elif i >= j: # 判断i>=j
# 交换虚拟中间值和j位置上的数,此时虚拟中间值变成真正中间值
data[start], data[j] = data[j], data[start]
break # 完成第一次排序,此时以中间值分左右两侧
quick(data, start, i - 1) # 调用快速排序函数,再快速排序左半边数据
quick(data, i + 1, end) # 调用快速排序函数,再快速排序右半边数据
data = [1, 3, 15, 20, 5, 4] # 定义列表并初始化
print("六名职员入职年限如下:")
print(data) # 输出原始数据
print("→→→→→→→→→→→→→→→→→→→→→")
quick(data, 0, (len(data) - 1)) # 调用快速排序,数据从位置0开始,到数据长度-1为止
print("从高到低排序之后的入职年限如下:")
print(data) # 输出排序后数据
print("→→→→→→→→→→→→→→→→→→→→→")
程序运行结果如图所示:
树形结构是元素结点之间有分支和层次关系的结构,类似于自然界的树。在生活中,有很多事物关系呈现树形结构,例如下图所示的家族关系,
还有下面所示的重庆大学教育学院的组织结构图,都可以使用树形结构来形象地表示。
树形结构是由 n 个元素组成的有限集合,如果 n=0,那么就称为空树;如果 n>0,树形结构应该满足以下条件:
向下面所示的树形结构的例子:
在介绍树形结构的条件时,提到了一个特殊的结点——根。上图所示的树形结构的根就是结点 a,就像树一样,树木要想长出茂密的枝条和叶子,就离不开树根。树形结构的形成也离不开根结点,只不过树是向上生长,而树形结构是从根往下描绘。上图中可以发现,根结点上面再也没有结点。
除了 a 这个根结点,上图中的树形结构还有结点 b,c,d,e,f,g,它们的共同点是不论是结点上面还是结点下面,至少都会存在 1 个与之连接的结点。并且为了能够结束树形结构,必须保证有一些无后续的结点,例如图中的结点 b,d,f,g。否则,就会变成一个无限的树形结构。
树的表示方法有四种形式:树形表示法、文氏图表示法、凹入表示法以及括号表示法,接下来一一进行介绍。
树形表示法
这是树的最基本的表示方法,用类似于如下图所示的一棵倒置的树来表示树形结构,这种方式非常直观、形象。
文氏图表示法
文氏图又称为 Venn图、温氏图、维恩图、范氏图,适合用来表示集合(或) 类之间的 大致关系
,也可以用文氏图来表示树形结构,例如,下图所示的就是上面所示的树形结构。
凹入表示法
括号表示法是将树形结构的根结点写在括号的左边,除根结点之外的其余结点写在括号中,并用逗号隔开。例如,
表示法的多样性,说明了树形结构在日常生活中及计算机程序设计中的重要性。一般来说,分等级的分类方案都可以用层次结构来表示,也就是都可以形成一个树形结构。
在后面讲解树的操作时,常常会用到一些术语,本节先简单介绍一些常用术语,参考如下图所示的树形结构:
蓝色球
就被称为结点。二叉树是一种特殊的树,比较适合计算机处理,任何树都可以转换成二叉树来处理,接下来我们来重点研究二叉树形结构。
二叉树依然是树形结构,它也适用上节中介绍的一些相关术语。但是二叉树还有一个条件:它的每个结点都有两个分支,左侧分支称为 左子树
;右侧分支称为 右子树
,因此二叉树的最大的度就是 2。如图所示:
二叉树有如下几个基本特性:
二叉树有几个特殊的类型:满二叉树、完全二叉树、斜二叉树、平衡二叉树。接下来我们一一进行介绍。
满二叉树
满二叉树除了最后一层的叶子结点,其余的结点都有 2 个结点,也就是每一层的结点都是满的,如下图所示:
在满二叉树中,第 k(k≥0)
层有 2k-1 个结点,例如 k=3 时有 23-1=4个结点。深度为 m 的满二叉树,有 2m-1 个结点,例如,当深度为 4 时,就有 24-1=15 个结点。具有 n 个结点的满二叉树,其深度为 [log2n]+1,例如,当结点为 15 时,其深度为:[log215]+1=4。
完全二叉树
完全二叉树与满二叉树不同的是,除最后一层外,每一层的结点都是满的,并且最后一层只缺少右边的若干结点。如图所示:
满二叉树可以说是完全二叉树,而完全二叉树不可以说是满二叉树。具有 n 个结点的完全二叉树,其深度为 [log2n]+1。
斜二叉树
当一个二叉树完全没有右结点或者左结点时,就被称为左斜二叉树或者右斜二叉树,第一个图所示的是左斜二叉树,第二个图所示的是右斜二叉树。
从上面两个图中可以看出,斜二叉树每层的结点数都是 1,有 n 个结点,二叉树的深度就是 n。
平衡二叉树
平衡二叉树既不是满二叉树也不是完全二叉树,平衡二叉树的左右子树的高度差不超过 1。如下图所示,是一个平衡二叉树。
说明:空二叉树就是没有结点的二叉树,空二叉树也是平衡二叉树的一种。二叉树是一种特殊的树,比较适合计算机处理,任何树都可以转换成二叉树来处理。
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父结点。从这句话来看,堆有两个条件:
先来看第一个条件:完全二叉树。完全二叉树(上面已经说过),就像一棵树,只不过二叉树的每个结点只有二个叉,且完全二叉树的生成顺序是从上到下、从左到右依次生成。如下图所示,就是一个完全二叉树。
其中结点 a 是结点 b、c 的父结点,结点 b 是 d、e 的父结点……,而反过来结点 b、c 是结点 a 的子结点,结点 d、e 是结点 b 的子结点….例如下图所示想要添加一个节点,如果添加到节点 q 的位置,就是错的,不满足完全二叉树的特点;而添加节点 w 的位置才能够满足完全二叉树的条件。
接下来看堆的第二个条件,子结点的键值或索引总是小于(或者大于)它的父结点,来举一个例子,如下图有这样一个完全二叉树:
从上图中可以看出:父结点 10 比它的子结点 5、8 都大,而父结点 5 比它的子结点 3、4 都大,父结点 8 比它的子结点 6 大。从图中也可以看出,它是一个完全二叉树,也满足子结点大于父结点的要求,称这样的结构为堆。根据堆结构的特点,可以通过一个公式确定某结点(例如:结点位置为 i) 的父结点和子结点的位置,公式如下:
求父结点公式:
父结点位置=(i-1)/2
求子结点公式:
左子结点=2*i+1
右子结点=2*i+2
来举一个例子,例如:给上图的堆结构编号,如下图所示:
想要求结点 5(它的编号是 i=1),求父结点带入公式如下:
父结点位置=(1-1)/2=0 # 位置0的数据是10
再求两个子结点的位置,带入公式如下:
左子结点=2*1+1=3 # 位置3的数据是3
右子结点=2*1+2=4 # 位置4的数据是4
从上面几种排序算法可以看出,排序法根本的过程就是交换,无论是哪种选择排序法,都离不开数据之间的交换。堆排序法也一样,也需要交换数据。同样,堆排序也有两种排序结果,即递增数列和递减数列,下面就两种结果形式的具体排序流程进行简要描述:
接下来用一组数据来详细讲解堆排序法。例如有这样一组数据 96、54、88、5、10、12,如图所示:
按照递增顺序进行排序,步骤如下:
步骤1:将上图中的数据放在一个完全二叉树的结构中,如下图所示:
步骤2:按照堆的特性交换数据,父结点要大于子结点,从上图来看,每个父结点的数据都比各自的子结点大,因此,父结点 96,就是数据的最大值。此时需要将此完全二叉树最低层的最右侧的数据与父结点进行交换,即数据 12 与 96 进行交换,如图所示:
步骤3:经历了上图的交换之后,将数据 96 分支砍掉,放到排序后的数列里,如下图所示:
步骤4:再来观察一下,父结点 12 与它的子结点 54、88 进行比较,先比较左结点,发现 54 大于 12,将位置就行交换,如下图所示。交换之后,再来看以数据 12 为父结点的分支,它的子结点 5、10 都比 12 小,因此位置不需要交换。
步骤5:再用父结点 54 与它的子结点 12、88 进行比较,88 比 54 大,再将 54 与 88 位置交换,如下图所示:
步骤6:交换完之后,88 为父结点,它目前是最大的数字,将数字 88 与当前完全二叉树最底层的最右侧的数据 10 进行交换,交换完之后,再将 88 分支砍掉,放在 96 前,此过程如下图所示:
步骤7:此时数据 10 为父结点,再次与它的子结点 12、54 进行比较,比较左子结点,12 比 10 大,交换数据 10 与 12 的位置,如下图所示。交换之后,再看看以 10 为父结点的分支,它的子结点 5 小于 10,因此不需要交换位置。
步骤8:在与右结点比较,数据 54 大于 12,将数据 12 与 54 进行比较,如图所示:
步骤9:交换如上图所示的状态之后,54 就是目前的最大值,再次需要将数据 54 与此时完全二叉树最底层最右侧的数据 5 交换,并砍掉 54 分支,放到数据 88 前,过程如下图所示:
步骤10:如上图所示,此时是以 5 为父结点,用它的子结点 10、15 比较,先来交换左子结点,如下图所示:
步骤11:再来比较右结点 12,12 大于 10,交换位置,如下图所示:
步骤12:此时数据 12 是最大值,将数据与最底层最右侧数据 10 进行交换,并砍掉数据 12 分支,放到数据 54 前,如图所示:
步骤13:此时只剩数据 10 和 5,上图中的二叉树也满足堆,因此直接将 10 放入到数据 12 前,5 放到数据 10 之前,最后的结果如下图所示:
此时已经用堆排序法将无序的数据进行递增排序,从步骤上看,每次都是用父结点和子结点比较、交换,接下来用 Python 实现堆排序。
【实例7】使用堆排序法为列表中的数字进行递增排序。具体代码如下:
"""
函数名称:heapify
功能:调整列表中的元素并保证以i为父结点的堆,并保证i是最大值
参数说明:heap:表示堆
heap_len:表示堆的长度
i:表示父结点的位置
"""
def heapify(heap, heap_len, i):
"""
给定某个结点的下标i,这个结点的父节点、左子结点、右子结点的下标都可以被计算出来
父结点:(i-1)//2
左子结点:2*i + 1
右子结点:2*i + 2 即:左子节点 + 1
"""
left = 2 * i + 1 # 左子结点位置
right = 2 * i + 2 # 右子结点位置
larger = i # 每次最大值赋给变量larger
# 左结点位置小于堆长度同时堆的最大值小于左子结点的值
if left < heap_len and heap[larger] < heap[left]:
larger = left # 此时将左结点位置给larger
# 右结点位置小于堆长度同时堆的最大值小于右子结点的值
if right < heap_len and heap[larger] < heap[right]:
larger = right # 此时将右结点位置给larger
if larger != i: # 如果做了堆调整则larger的值等于左节点或者右节点的值
heap[larger], heap[i] = heap[i], heap[larger] # 这个时候做堆调整操作
# 递归对各分支做调整
heapify(heap, heap_len, larger)
def build_heap(heap): # 构造一个堆,将堆中所有数据重新排序
heap_len = len(heap) # heapSize是堆的长度
for i in range((heap_len - 2) // 2, -1, -1): # 自底向上建堆
heapify(heap, heap_len, i)
def heap_sort(heap): # 将根节点取出与最后一位做对调,对前面len-1个节点继续进行堆调整过程
build_heap(heap) # 调用函数创建堆
# 调整后列表的第一个元素就是这个列表中最大的元素,将其与最后一个元素交换,然后将剩余的列表再递归的调整为最大堆
for i in range(len(heap) - 1, -1, -1):
heap[0], heap[i] = heap[i], heap[0]
heapify(heap, i, 0)
data = [96, 54, 88, 5, 10, 12]
print("原始数据为:")
for k in range(6): # 遍历原有数据
print('%4d' % data[k], end='') # 输出结果
print('\n---------------------------')
print("排序之后的结果为:")
heap_sort(data)
for k in range(6): # 遍历排序后数据
print('%4d' % data[k], end='') # 输出结果
运行结果如下图所示:
【练习6】练习:姓氏排名。百家姓从小就背:赵、钱、孙、李、周、吴、郑、王……,但是之前看过一个新闻,按照中国人口最多姓氏排名,并不是这样一个顺序,接下来根据人口的数量对姓氏排名。具体代码如下:
"""
函数名称:heapify
功能:调整列表中的元素并保证以i为父结点的堆,并保证i是最小值
参数说明:heap:表示堆
heap_len:表示堆的长度
i:表示父结点的位置
"""
def heapify(heap, heap_len, i):
"""
给定某个结点的下标i,这个结点的父结点、左子结点、右子结点的下标都可以被计算出来
父结点:(i-1)//2
左子结点:2*i + 1
右子结点:2*i + 2 即:左子节点 + 1
"""
left = 2 * i + 1 # 左子结点位置
right = 2 * i + 2 # 右子结点位置
minimum = i # 每次最小值赋给变量minimum
# 左结点位置小于堆长度同时堆的最小值大于左子结点的值
if left < heap_len and heap[minimum] > heap[left]:
minimum = left # 此时将左结点位置给minimum
# 右结点位置小于堆长度同时堆的最小值大于右子结点的值
if right < heap_len and heap[minimum] > heap[right]:
minimum = right # 此时将右结点位置给minimum
if minimum != i: # 如果做了堆调整则minimum的值等于左结点或者右结点的值
heap[minimum], heap[i] = heap[i], heap[minimum] # 这个时候做堆调整操作
# 递归对各分支做调整
heapify(heap, heap_len, minimum)
def build_heap(heap): # 构造一个堆,将堆中所有数据重新排序
heap_len = len(heap) # heapSize是堆的长度
for i in range((heap_len - 2) // 2, -1, -1): # 自底向上建堆
heapify(heap, heap_len, i)
def heap_sort(heap): # 将根节点取出与最后一位做对调,对前面len-1个节点继续进行堆调整过程
build_heap(heap) # 调用函数创建堆
# 调整后列表的第一个元素就是这个列表中最大的元素,将其与最后一个元素交换,然后将剩余的列表再递归的调整为最小堆
for i in range(len(heap) - 1, -1, -1):
heap[0], heap[i] = heap[i], heap[0]
heapify(heap, i, 0)
data = [6460, 8890, 5540, 9530, 8480]
data2 = ['刘', '王', '陈', '李', '张']
print("姓氏以及人口数量(单位:万人):")
for datas2 in data2:
print(" ", datas2, end=' ')
print()
for k in range(5): # 遍历原有数据
print('%6d' % data[k], end='') # 输出结果
print('\n---------------------------')
print("排序之后姓氏以及对应人口数量如下:")
heap_sort(data)
for k in range(5): # 遍历排序后数据
print('%6d' % data[k], end='') # 输出结果
print()
data3 = ['李', '王', '张', '刘', '陈']
for datas3 in data3:
print(" ", datas3, end=' ')
前七种排序方法都是基于数据之间进行比较交换进行排序,而接下来介绍的计数排序和基数排序都是非交换的排序。本篇博文先来看一下计数排序法。计数排序的主要思想是将待排序数据值转化为键,存储在额外开辟的数组空间中。计数排序要求输入的数据必须是有确定范围的整数,因此计数排序法适用于量大范围小的数据,例如公司员工入职年限问题,公司员工年龄问题,高考排名问题等等。
接下来用一组数据来详细讲解计数排序法。例如有这样一组数据1、2、4、1、3、5、2、2、7、3、4,如下图所示:
按照递增顺序进行排序,步骤如下:
步骤1:查看原始数据最大值和最小值,确定范围。上图中的最大值是 7,因此范围是从 0~7,那么就准备 8 个桶,并且给桶进行编号,如图所示:
步骤2:将对应的数字依次放到各个桶中,并且开始计数,例如第一个数字 1,放到 1 号桶且计数个数是 1;第二个数字是 2,放到 2 号桶且计数个数是 1;第三个数字 4,放在 4 号桶并计数个数 1;第四个数字是 1,依然放在 1 号桶中,此时计数个数升为 2,如下图所示:
步骤3:按照第 2 步的规律,将后续的数字依次放入各个桶中,并计数个数,最终的 7 个桶情况以及计数个数情况如图所示:
步骤4:放入桶中之后,从1号桶~7号桶,依次取出数据。例如先去 1 号桶中的两个数字 1,然后取出 2 号桶中的三个 2……,按照此规律将全部数据取出,最终取出的结果如图所示:
从上图的结果来看,已经将数据排序好,这就是计数排序法的过程。接下来用 Python 代码实现计数排序法。
【实例8】使用计数排序法为列表中的数字进行递增排序。具体代码如下:
def count_sort(data, maxValue): # 定义计数排序,data是列表数据,maxValue表示最大值
bucket_len = maxValue + 1 # 定义桶的长度是最大值加1,桶号从0开始
bucket = [0] * bucket_len # 初始化桶
count = 0 # 计数个数
arr_len = len(data) # 列表长度
for i in range(arr_len): # 遍历列表
if not bucket[data[i]]: # 列表数据不为桶号
bucket[data[i]] = 0 # 这时初始化从0将列表数据做桶号
bucket[data[i]] += 1 # 桶号依次加1
for j in range(bucket_len): # 遍历桶
while bucket[j] > 0: # 将列表数据放在对应桶号内
data[count] = j
count += 1 # 计数个数加1
bucket[j] -= 1 # 个数减一,下一个相同的元素往前排
return data # 返回排序后的列表
data = [1, 2, 4, 1, 3, 5, 2, 2, 7, 3, 4]
print("排序前列表数据:")
for i in range(11):
print("%2d" % data[i], end="")
print()
data2 = count_sort(data, 7) # 调用计数排序函数
print("排序后列表数据:")
for j in range(11):
print("%2d" % data2[j], end="")
运行结果如下图所示:
从上图来看,最后的运行结果与文章刚开始分析的结果一模一样。
基数排序法和计数排序法一样,都是一种非交换排序。基数排序过程和计数排序过程都需要借助桶来进行。基数排序的主要思想是设置若干个桶,将关键字为 k 的记录放入第 k 个桶,然后按序号将非空的数据连接。关键字 k 就是将每个数据按个位、十位、百位…进行分割而产生的。基数排序不仅仅可以应用在数字之间的排序,还可以应用在字符串排序(按26个字母顺序)等。
接下来用一组数据来详细讲解基数排序法。例如有这样一组数据 410、265、52、530、116、789、110,如下图所示:
按照递增进行排序,步骤如下:
步骤1:无论是个位、十位、百位都是由数字 0~9 组成,因此桶号也应该是从 0~9,例如创建桶如图所示:
步骤2:将原始数据中的数字先按个位数分类,即数字 410 的个位是 0,数字 256 的个位是 6,数据 52 的个位是 2,按照此规律得知,此数据的个位数分别是 0、5、2、0、6、9、0,将其放在对应的桶中,如下图所示:
步骤3:按照桶号顺序,将数据从各个桶中取出来。取出的数据顺序如图所示:
步骤4:再将上图中取出来的数据按十位数分类,即 410 的十位是 1,530 的十位是 3,按照此规律得知,此数据的十位数分别是 1、3、1、5、6、1、8,将其放在对应的桶中,如下图所示:
步骤5:按照桶号顺序,将数据从各个桶中取出来。取出的数据顺序如图所示:
步骤6:再将上图中取出来的数据按百位数分类,即 410 的百位是 4,110 的百位是 1,按照此规律得知,此数据的百位数分别是 4、1、1、5、0、2、7,将其放在对应的桶中,如图所示:
步骤7:按照桶号顺序,将数据从各个桶中取出来。取出的数据顺序如图所示:
从上图中的结果来看,已经将数据排序好,这就是基数排序法的过程。接下来用 Python 代码实现基数排序法。
【实例9】使用基数排序法为列表中的数字进行递增排序。具体代码如下:
def radix_sort(data): # 基数排序,参数data是待排序数列
i = 0 # 记录当前正在排拿一位,最低位为1
max_num = max(data) # 最大值
j = len(str(max_num)) # 记录最大值的位数
while i < j:
# 初始化桶数组,因为每一位数字都是0~9,故建立10个桶,列表中包含十个列表元素
bucket_list = [[] for x in range(10)]
for x in data: # 找数据s
bucket_list[int(x / (10 ** i)) % 10].append(x) # 找到位置放入桶数组
print(bucket_list) # 打印每次的桶情况
data.clear() # 将原data置空
for x in bucket_list: # 放回原data序列
for y in x: # 遍历排序后的结果
data.append(y) # 放数据
i += 1 # 执行一次,向后继续拿数据执行循环
data = [410, 265, 52, 530, 116, 789, 110] # 待排序列表
radix_sort(data) # 调用基数排序函数
print(data) # 输出排序结果
运行结果如下图所示:
从结果上看,每行有十个列表,表示 0~9 号桶,每个列表包含的数据就是上述步骤放入桶中的数据,完全符合上述讲解的步骤。
人工智能
是当今时代的流行词汇,这也使很多想要在人工智能领域有所成就的大学生选择了计算机类专业。在我们身边,就会有很多人工智能应用的例子。例如,医院里,医生借助 AI 辅助诊断患者是否患有疾病;街道上,公共部门利用机器人喷洒消毒液;高速路上,交警使用无人机巡逻、疏导车辆等。
人工智能
是计算机的一个分支,自然也离不开程序语言,程序语言非常强大(如下图所示)
可以用于 Web 开发、游戏开发、为桌面应用程序构建脚本和 GUI、配置服务器、执行科学计算和进行数据分析等。可以说,程序语言几乎可以用于做任何事情!那么程序语言为何如此强大?这就离不开程序中 精美绝伦
的算法。因此说,一个成功的程序背后必会有一个好的算法。
目前,现代生活已经非常依赖信息技术了,似乎计算机什么都能干,但稍稍了解计算机内部结构的人就会知道,其实计算机只是比较 听话
,它并不知道自己在做什么,使用者让它去做什么动作它就会执行什么动作。而能够让计算机系统变得无所不能的是各种各样的算法,如图所示:
人类用智慧设计的算法,造就了计算机的 智能
。因此,人类告诉计算机以什么样的顺序去执行某些动作,这就是我们通常说的 算法
。
要想在编程之路上走得长远就必须拥有编程思维,那么究竟什么是编程思维?虽然计算机相关的学者至今没有一个明确的定义,但我们可以将编程思维理解成人类的思想方式,而算法是计算机编程思维的一种表现。从当前计算机应用的水平来看,人们已经设计出许多非常 聪明
的算法,极大提高了我们解决问题的能力,但仍有许多复杂问题依然期待人类给出更有效的算法。
算法是计算机科学中的核心理论之一,也是人类使用计算机解决问题的技巧之一。算法不仅可以应用在计算机领域中,还可以应用在数学、物理等一些学术领域中,不仅如此,其实在我们的生活中,也是在时时刻刻使用算法。例如,大厨制作美食的过程、制定工作计划、设计精美页面流程等,都在无形中进行着算法操作。本小节将从搜索信息、通信、工业、数学等方面来介绍算法的作用。
2.1 搜索信息方面
当今是大数据覆盖的时代,算法加数据能演化出 五花八门
的应用。例如,我们最熟悉的搜索引擎——百度,如下图所示:
高效的算法让用户能够精准地找到想要搜索的信息,如果没有这些 聪明
的算法,用户将迷失在互联网这个巨大的数据森林中。
2.2 通信方面
算法不仅在搜索信息方面有所成就,在通信方面亦是如此。如果没有天才的编码和加密算法,我们也不可能在网络上安全地通信,天气预报也不能够如此准确。
2.3 工业方面
工业生产需要大量的劳动力来推动生产线的运作。而如何对生产线进行有序管理、保障产品质量、提高生产效率就成为; 工业生产中的重中之重。工业自动化管理系统通过大量精密算法的使用,能够智能地对生产中的各个环节进行管理、监控、优化、完善,如下图所示:
2.4 数学方面
算法领域巨大的进步就是来自于美好的思想,它指引我们更有效地解决数学问题。数学领域的问题并不局限于算术计算,还有很多表面不是数学化的问题,例如:
千年虫
问题这些问题非常具有挑战性,需要逻辑推理、几何与组合想象力,才能解决问题,这些就是设计算法所需要的主要能力。
2.5 其他方面
除此之外,工业机器人、汽车、飞机以及几乎所有家用电器中都包含的许多的微处理器都是依赖算法才能发挥作用。例如:飞机中成百上千的微处理器在算法的帮助下控制引擎、减少能耗、降低污染等;微处理器能控制汽车的制动器和方向盘,提高稳定性和安全性;微处理器可以代替人类实现汽车无人驾驶。微处理器的强大背后离不开完美的算法。
所以说,算法很强大,学好算法,你可以编写出健壮的程序,工作中也不会畏惧更加严峻的挑战。
许多人认为学习编程就是学习最新的编程语言、技术和框架,其实计算机算法更重要。从 C 语言、C++、Java 到 Python,虽然编程语言种类很多,但是亘古不变的是算法。所以修炼好算法这门 内功
,再结合编程语言这些 招式
,才能称霸 编程武林
。本节就来介绍算法的基础。
3.1 算法的定义
算法是一组完成任务的指令,因此有计算机工作者这样对其进行定义:为了解决某个或某类问题,需要把指令表示成一定的操作序列,操作序列包括一组操作,每一个操作都会完成特定的功能。简单来说,算法是解决特定问题步骤的描述,即处理问题的策略。
例如:经典问题——百钱买百鸡。说明:这篇博文中的分析过程到代码的实现,整个过程就是算法的过程。
3.2 算法的特性
算法是解决 做什么
和 怎么做
的问题,解决一个问题可能有不同的方法,但是算法分析最为核心的是算法的速度。因此解决问题的步骤是需要在有限时间内能够完成的,并且操作步骤中不可以有不明确的语句,使得步骤无法继续进行下去。通过对算法概念的分析,可以总结出一个算法必须满足如下五个特性。如下图所示。本节就来介绍这五大特性。
输入。一个程序中的算法和数据是相互联系的,算法中需要输入的是数据的值。输入数据可以是多个,也可以是零个,其实输入零个数据并不表示这个算法没有输入,而是这个输入没有直观地显现出来,隐藏在算法本身当中。例如,Python 语言中用 input() 函数向控制台上输入数据,代码如下:
name = input("请输入您的姓名:") # 输入变量值
确定性。一个算法中的每一个步骤的表述都应该是确定的。在人们的日常生活中,遇到语意不明确的语句,虽然可以根据常识、语境等理解,但是还有可能理解错误。如下图所示,在中国的社交中,熟人见面经常会问:“吃了没?” 如果是不了解中国文化的外国人就很难理解这句话,这是问吃什么呢?吃饭?吃水果?也不确定这个问句是问谁的。这句话没有确定性,既没有主语,也没有宾语。人遇到这样的问题都很难理解,何况计算机了。计算机不比人脑,不会根据算法的意义来揣测每一个步骤的意思,所以算法的每一步都要有确定的含义。
输出。输出就是算法实现所得到的结果。算法没有输出是没有意义的。有的算法输出的是数值;有的输出的是图形,有的输出则不显而易见。例如:
print("1314") # 输出数值
print("^ _ ^") # 输出图形
print(" ") # 输出空格,不显而易见
有限性。一个算法在执行有限步骤后能够实现,或者能够在有限时间内完成,就称为该算法具有有限性。例如,在 百钱买百鸡
代码的 for 循环中(1,20)、(1, 33)、(3, 98, 3) 这几个范围,控制了这段程序的有限性。如果没有此条件,for 循环就会无终止地循环,这样程序就进入了死循环,不满足算法的有限性。
有的算法在理论上满足有限性的,在有限的步骤后能够完成,但是实际上计算机可能会执行一天、一年、十年、甚至更久的时间。算法的核心就是速度,那么这个算法也就没有意义了。总而言之,有限性没有特定的限度,主要取决于使用者的需要。
有效性。算法的有效性就是指每一个步骤都能够有效地执行,并且得到确定的结果,还能够用来方便地解决一类问题。例如:下面这段程序代码中的 z=x/y
就是一个无效的语句,因为 0 是不可以作为分母的。
3.3 算法性能分析与度量
算法是解决问题的方法。但是解决问题的方法不止一个,方法多了,自然而然就有了优劣之分。例如,当一个人在扫地的时候,人们不会发现这个人扫地的好与坏。然而,有两三个人同时做这个工作的时候,人们就有了比较,就可以根据不同的评定标准评价工作的优劣。有人认为 A 好,因为他扫得快;有人认为 B 好,因为他扫得干净等。
那么,对于算法的优劣怎么来评定呢?下面就从算法的性能指标和算法效率的度量这两个方面来介绍。
算法的性能指标
评定一个算法的优劣,主要有以下几个指标:
算法效率的度量
度量算法效率的方法有两种:
第一,事后计算。先实现算法,然后运行程序并测算其时间和空间的消耗。这种度量方法有很多弊端,由于算法的运行与计算机的软件、硬件等环境因素有关,不容易发现算法本身的优劣。同样的算法用不同的编译器编译出的目标代码不一样多,完成算法所需的时间也不同,并且当计算机的存储空间小时,算法运行时间就会延长。
第二,事前分析估算。这种度量方法是通过比较算法的复杂性来评价算法的优劣,算法的复杂性与计算机软硬件无关,仅与计算时间和存储需求有关。算法复杂性的度量可以分为 空间复杂度度量
和 时间复杂度度量
。
算法的时间复杂度
算法的时间复杂度度量主要是计算一个算法所用的时间,主要包括程序编译时间和运行时间。由于一个算法一旦编译成功可以多次运行,因此忽略编译时间,在这里只讨论算法的运行时间。
算法的运行时间依赖于加、减、乘、除等基本的运算,以及参加运算的数据大小、计算机硬件和操作环境等。所以要想准确地计算时间是不可行的,我们可以通过计算影响算法运行时间作为主要的因素:问题的规模,也就是输入量的多少,来计算算法的时间复杂度。
同等条件下,问题的规模越大运行的时间也就越长。例如,求 1+2+3+…+n 的算法,即 n 个整数的累加求和,这个问题的规模为 n。因此,运行算法所需的时间 T 是问题规模 n 的函数,记作 T(n)。
为了客观地反映一个算法的执行时间,通常用算法中基本语句的执行次数来度量算法的工作量。而这种度量时间复杂度的方法得出的不是时间量,而是一种增长趋势的度量。当 n 不断变化时,T(n) 也会不断变化。但有时我们想知道它变化时将呈现什么规律。为此,我们引入时间复杂度概念。一般情况下,算法中基本操作重复执行的次数是问题规模 n 的某个函数,用 T(n) 表示,若有某个辅助函数 f(n),使得当 n 趋近于无穷大时,T(n)/f(n) 的极限值为不等于零的常数,则称 f(n) 是 T(n) 的同数量级函数。记作 T(n)=O(f(n)),称 O(f(n)) 为算法的渐进时间复杂度,这种 O(f(n)) 表示法被称为大O(大写的字母O)表示法。
算法的空间复杂度
算法的空间复杂度是指在算法的执行过程中,需要的辅助空间数量。辅助空间数量指的不是程序指令、常数、指针等所需要的存储空间,也不是输入数据所占用的存储空间。辅助空间是除算法本身和输入输出数据所占据的空间以外的算法临时开辟的存储空间。算法的空间复杂度分析方法同算法的时间复杂度相似,设 S(n) 是算法的空间复杂度,通常可以表示为:
S(n) = 0(f(n))
例如:当一个算法的空间复杂度为一个常量,即不随被处理数据量 n 的大小而改变时,可表示为 O(1);当一个算法的空间复杂度与以 2 为底的 n 的对数成正比时,可表示为 O(log2n);当一个算法的空间复杂度与 n 成线性比例关系时,可表示为O(n)。
4.1 概念与举例
概念。大O表示法是一种特殊的表示法,它表示算法的时间复杂度(即速度)。说明:大O表示法是对算法性能的一种粗略估计,并不能精准地反映某个算法的性能。
举例。看到这里,大家可能存在疑惑,大O表示法是怎么表示的?接下来我们用一个程序介绍大O表示法是怎样表示该程序的时间复杂度(请读者用函数的角度思考以下讲解内容)。
【实例1】计算 a、b、c 的值。例如:a+b+c=1000 且 a2+b2=c2(a、b、c 都是自然数),求 a、b、c 的可能组合(a、b、c 的范围在 0~1000 之间)。代码如下:
import time
start_time = time.time()
# 注意是三重循环
for a in range(0, 1001): # 遍历a
for b in range(0, 1001): # 遍历b
for c in range(0, 1001): # # 遍历c
if a ** 2 + b ** 2 == c ** 2 and a + b + c == 1000: # 条件
print("a, b, c: %d, %d, %d" % (a, b, c))
end_time = time.time()
print("elapsed: %f" % (end_time - start_time))
print("complete!")
最终需要大约 532 秒(不同计算机的运行时间不同)之后才能运行出结果,结果如下图所示:
将这段代码的进行如下修改:
import time
start_time = time.time()
# 注意是三重循环
for a in range(0, 1001): # 遍历a
for b in range(0, 1001): # 遍历b
c = 1000 - a - b # 用表达式求解c
if a ** 2 + b ** 2 == c ** 2 and a + b + c == 1000: # 条件
print("a, b, c: %d, %d, %d" % (a, b, c))
end_time = time.time()
print("elapsed: %f" % (end_time - start_time))
print("complete!")
第二段代码最终运行的结果所需的时间不到 1 秒钟,结果依然是上图所示的内容。从速度上看,第二段代码更快,也就是说,第二段代码算法要比第一段代码算法更成熟,意义上更好一些。那接下来从时间上分析一下这两段代码:
第一段代码的时间复杂度:设时间为 f,这段代码用到了 3 个 for 循环,每个循环遍历一次运行的时间复杂度是 1000,3 次嵌套 for 循环的时间复杂度是每层 for 循环时间复杂度相乘,即 T=1000*1000*1000。而 for 循环之后的 if 和 print() 两条语句,我们暂且算成是2。因此这段程序的时间复杂度是:
f = 1000 * 1000 * 1000 * 2
如果将 for 循环的范围写成 0~n,那么时间复杂度就变成了一个函数,其中 n 是一个变量,可以写成如下形式,也等价于 f(n)=n3*2:
f(n)=n3*2 函数在象限图的分布如下图所示:
从图像可以看到,这个函数的走势是不变的,而系数无论是 2 还是1,只不过是使这个走势更陡峭一些,并不影响大趋势,因此可以忽略系数 2。那么图像所示的走势基本可以说是 f(n)=n3 的象限图,增加的系数形成的象限图只不过是f(n)=n3 的渐近线(如上图所示的红色线),对应的函数就是渐近函数,将一系列表示时间复杂度的渐近函数用 T(n) 来表示,就变成了如下形式(k 表示系数):
前面提过,系数 k 并不影响函数走势,所以这里可以忽略 k 的值,最终 T(n) 可以写成如下形式:
这种形式就是大 O 表示法,f(n) 是 T(n) 的 大O 表示法。其中的 O(f(n)) 就是这段代码的渐近函数的时间复杂度,简称时间复杂度,也就是 T(n)。通过上面对算法时间复杂度的分析,总结出这样一条结论,在计算任何算法的时间复杂度时,可以忽略所有低次幂和最高次幂的系数,这样可以简化算法分析,并使注意力集中在增长率上。
第二段代码的时间复杂度:第二段代码有 2 层 for 循环,忽略系数,它的时间复杂度就是 T(n)=n2,最终的时间复杂度 f(n)=O(n2),这段代码的复杂度的走势如下图所示:
例如:这样一段代码:
求这段代码的时间复杂度 T(n),分析如下:
根据大O表示法推导形式(忽略所有低次幂和最高次幂的系数,包括常数),最终的大O表示法是 T(n)=O(n2)。象限图和上图所示的一样。 常见的几个大O表示法如下:
在本篇博文中一共介绍了 9 种排序算法,那么这 9 种排序算法的各种复杂度是怎样的呢?本节就来总结各种算法的复杂度以及稳定情况。 说明:假设需要排序的数列长度为 n,各种排序算法复杂度如下表所示:
多学两招:辅助记忆法:冒泡、选择、直接排序需要两个 for 循环,每次只关注一个元素,平均时间复杂度为 O(n2)(一遍找元素O(n),一遍找位置O(n))。快速、合并、希尔基于二分思想,平均时间复杂度为 O(nlog(n))(一遍找元素O(n),一遍找位置O(logn))。计数排序、基数排序是线性阶(O(n))排序。