前言
本篇文章以图文结合的方式,分析了二分查找、合并排序和快速排序的逻辑,给出了Python实现代码及其复杂度分析。参考自书籍 Fundamentals of Python Data Structure ,本篇文章代码均已测试通过。
⚠️注意:在本系列的基本排序算法介绍中,常用到 swap()
功能函数,建议您在运行本文章代码时使用后缀名为 .ipynb 的源文件,并总是先运行 swap()
功能函数 所在的单元格。
👉附上代码实现 swap()
:
def swap(lyst, i, j):
""" 交换列表中两元素的位置 """
temp = lyst[i]
lyst[i] = lyst[j]
lyst[j] = temp
引入
在正式介绍算法的设计逻辑和给出实现代码之前,让我们以图书馆中找书的情景为例。图书馆中的书基本上都按照出版社、图书类别等编号排序分层被放置好,然而有些时候,读者阅读完书籍后并不会把书放回原位、而会选择距离较近的书架放置。假设我们现在要找的目标书籍曾被多位读者阅读后,被放置在距离原位不远处的某层书架上,该层书架所在的那一排并未被管理人员整理,并且我们事先未知。
- 首先,按一般步骤走:
- 在检索机器上查找图书编号和楼层
- 根据图书编号首字母在相应楼层找一排书架
- 根据图书编号的一(二)位数字找某个书架
- 在书架的某一行上找到目标书籍
- 然而,在图书编号对应的位置上并没有发现目标
- 往该排书架的左(右)侧移动到一个书架
- 从该书架的中间位置开始
- 先从左侧的起始位置向右移动至该书架的中间位置
- 再从右侧的起始位置向左移动至该书架的中间位置
- 重复上述过程,直至找到目标书籍
首先我们在原位没发现目标书籍时,会下意识地往旁边的书架或在该书架的其他位置找——因为图书的初始状态是分类放置的,如计算机科学丛书中的操作系统类书籍都汇总在一排书架,在它周围的另一排书架上可能放置着程序设计语言类书籍,来这里阅读的读者大都对操作系统或程序设计等计算机科学子类书籍感兴趣。又根据本情境中的前提条件“一排书籍并未被有序整理”,所以我们可以在不知道目标书籍被放在哪里的情况下,在该位置周围以一种方法寻找;一种较为快速的方法是“二分查找”。
该方法的核心思想是:从某行/列的中间位置作为分界点,先后从左侧和右侧以升序/降序(一般为升序)的方式顺序查找,最好的情况是刚好在左侧的起始位置找到目标,最坏的情况是搜索完整行/列才找到目标。相较于原先从该行/列的起始位置到结束位置的查找方式,二分查找一次只需查找一半的数据,且查找次数大都介于最好和最坏情况之间,也就是说,使用二分查找消耗的时间小于等于顺序搜索的时间。
👉顺序搜索的代码实现请看系列文章 Python数据结构之基本排序算法(一)
四、二分查找有序列表
二分查找(binary search)是合并排序算法的预备思想,前者用以提高查找目标值的效率,前提是在有序列表中;而后者将待排序的列表以二叉树的形式分割后排序再合并,目的是对列表进行排序。
📚图解如下所示:
在文章 Python数据结构之基本排序算法(一) 中,我从优化顺序搜索的角度给出了二分查找的实现代码、但没有对其作进一步的解释,而在这篇文章中,我在介绍合并排序算法之前结合情景对二分查找的思想进行了解释,并且用了一个计算时的小技巧另外实现。
💻代码实现:
""" 二分查找 """
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
left = 0
right = len(nums) - 1
while left <= right:
midpoint = (right - left) // 2 + left
if target < nums[midpoint]:
right = midpoint - 1
elif target == nums[midpoint]:
return midpoint
else:
left = midpoint + 1
return -1
五、合并排序
合并排序(merge sort)的基本思想:计算一个列表的中间位置,并递归地排序其左边和右边的子列表(分而治之),将两个排序好的子列表合并为一个排好序的列表。当子列表不能再划分时,停止分割。
📚图解如下所示:
从上述图解中,我们看到拆分好子列表后再合并这个过程并不是在原列表中进行的,因为每一次合并都相当于重新分配存储空间、合并后的列表长度始终在变化,那么,我们就需要一个数据缓冲区(copyBuffer)存储子列表排序合并的结果,而且为避免在合并排序的过程中多次使用copyBuffer分配和释放空间所带来的空间复杂度指数增加问题,我们将其作为函数参数传入。
💻代码实现:
""" 合并排序法:用了递归、分而治之的策略突破复杂度为O(n^2)。
在一个列表中间分割成左右子列表,递归排序在合并,直至子列表不能分割为止 """
print("{0:=^30}".format("合并排序法"))
import numpy as np
""" 分割成若干个左右子列表 """
def mergeSortHelper(lyst, copyBuffer, low, high):
# copyBuffer 在合并过程中需要的数据缓冲区
# low, high 子列表的边界点索引
# midddle 子列表的中点索引
if low < high:
middle = (low + high) // 2
mergeSortHelper(lyst, copyBuffer, low, middle)
mergeSortHelper(lyst, copyBuffer, middle+1, high)
merge(lyst, copyBuffer, low, middle, high)
""" 将所有的子列表升序排序并返回一个新的列表 """
def mergeSort(lyst):
copyBuffer = np.ndarray(len(lyst))
mergeSortHelper(lyst, copyBuffer, 0, len(lyst)-1)
return lyst
""" 将两个已排好序的子列表合并 """
def merge(lyst, copyBuffer, low, middle, high):
# 将i1和i2分别初始化为两个子列表的起始索引
i1 = low
i2 = middle + 1
# 按照原有顺序将子列表中的数据交错送入数据缓冲区中
for i in range(low, high+1):
if i1 > middle:
copyBuffer[i] = lyst[i2] # 左子列表已满,进入右子列表
i2 += 1
elif i2 > high:
copyBuffer[i] = lyst[i1] # 右子列表已满,到达列表末尾
i1 += 1
elif lyst[i1] < lyst[i2]:
copyBuffer[i] = lyst[i1] # 左子列表的元素入区
i1 += 1
else:
copyBuffer[i] = lyst[i2] # 右子列表的元素入区
i2 += 1
for i in range(low, high+1):
lyst[i] = copyBuffer[i] # 将数据项按照合并后的顺序从缓冲区copy到原列表中
l = [38, 23, 12, 45, 67, 9, 56]
l1 = mergeSort(l)
print(type(l1))
for k in range(len(l)):
print("%d " % l1[k], end="")
👀输出为:
============合并排序法=============
<class 'list'>
9 12 23 38 45 56 67
六、快速排序
快速排序(Quick Sort)的基本思想如下:
- 首先,选取列表的中点作为基准点(pivot,注:是值而不是索引),做初始化(Init,在图解中有介绍)
- 设置边界点索引(boundary),以此作为分割列表的分界线。顺序搜索列表,当检索到列表中的值小于基准点时,交换边界点索引对应的值并将边界点右移一位(索引加一),最后将基准点与边界点索引对应的值交换位置,保证边界点左边的值都小于基准点、右边的值都大于基准点,完成左右子列表的分区
- 分而治之。对于在基准点左侧的左子列表和右侧的右子列表先后递归调用上述过程
- 每次遇到少于2个项的子列表就停止
📚图解如下所示:
💻代码实现:
""" 快速排序法:建立基准点和边界,将基准点放在最后与边界形成排序区间,每遍历一轮将小的数与第一项交换并前移边界,
最坏的情况下,其复杂度为O(n^2),使用递归后是O(n)"""
print("{0:=^30}".format("快速排序法"))
def paritition(lyst, left, right):
# 初始化:设置中点为基准点并将其与列表最右侧的值交换位置
middle = (left + right) // 2
pivot = lyst[middle]
lyst[middle] = lyst[right]
lyst[right] = pivot
# 将列表最左侧位置设置为boundary
boundary = left
# ===================初始化操作完成==============================
# 将小于基准点的数据元素移动至左边
for index in range(left, right):
if lyst[index] < pivot:
swap(lyst, index, boundary)
boundary += 1
# 交换基准点到边界点索引位置
swap(lyst, right, boundary)
return boundary
def quicksortHelper(lyst, left, right):
if left < right:
pivotLocation = paritition(lyst, left, right)
quicksortHelper(lyst, left, pivotLocation - 1)
quicksortHelper(lyst, pivotLocation + 1, right)
def quicksort(lyst):
quicksortHelper(lyst, 0, len(lyst) - 1)
for k in range(len(lyst)):
print("%d " % lyst[k], end="")
print("\n")
s4 = quicksort([12, 19, 17, 18, 14, 11, 15, 13, 16])
👀输出为:
============快速排序法=============
11 12 13 14 15 16 17 18 19