从查找算法的性能可以看出,有序数据可以提高查找速度。对数据进行排序,是数据结构与算法知识基础篇中最后介绍也是最重要的一部分。
面试时考察编程基础,一看字符串、数组处理的一些题目,二看链表、树的基础应用,三看查找、排序各种方法张口就来。
排序算法是和语言无关的,本节重点还是python的实现;另外,排序算法分为几大类,若有不理解之处还要自行研究,本文不对原理详细展开(原理上比较复杂的算法不多,大多数极易理解)。
参考博客
十大经典排序算法最强总结
十大经典排序算法(Python代码实现)
Python实现十大经典排序算法
排序甚至有个定义:对于一组记录组成的表,表中每项记录有一项可以用来标识大小关系(称为关键字),也就是说每项记录还有其他附加信息,通过整理表中的数据使之按关键字递增或递减有序排列。
稳定性:当待排序的表中有多个关键字相同的记录,经过排序后,这些具有相同关键字的记录之间的相对次序保持不变,则称该排序方法是稳定的。注意: 稳定性是针对所有输入实例而言的。
外排序与内排序:在排序中,若整个表都是放在内存中处理的,则称之为内排序;若排序过程中要进行数据的内、外存交换,则称之为外排序。
一般将排序算法分为插入排序、选择排序、交换排序、归并排序、基数排序五大类;其中基数排序属于非比较排序,其他都为比较排序。
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。
计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr之前有多少个元素,则唯一确定了arr在排序后数组中的位置。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。
非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。
我们看一下主要的十类排序算法的比较,个中细节还是不少,在理解算法原理后,总结如下几条随便谈谈:
是一种简单直观的交换排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
def bubbleSort(nums):
for i in range(len(nums) - 1): # 遍历 len(nums)-1 次
for j in range(len(nums) - i - 1): # 已排好序的部分不用再次遍历
if nums[j] > nums[j+1]:
nums[j], nums[j+1] = nums[j+1], nums[j] # Python 交换两个数不用中间变量
return nums
快排属于交换排序,跟冒泡排序是类似的交换思路。是由东尼·霍尔所发展的一种排序算法。
快排是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。它是处理大数据最快的排序算法之一,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。它的主要缺点是非常脆弱。
重点学习一下,面试必考。
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,这个分割的数称为pivot;然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
def quickSort(nums, left, right): # 这种写法的平均空间复杂度为 O(logn)
# 分区操作
def partition(nums, left, right):
pivot = nums[left] # 基准值
while left < right:
while left < right and nums[right] >= pivot:
right -= 1
nums[left] = nums[right] # 比基准小的交换到前面
while left < right and nums[left] <= pivot:
left += 1
nums[right] = nums[left] # 比基准大交换到后面
nums[left] = pivot # 基准值的正确位置,也可以为 nums[right] = pivot
return left # 返回基准值的索引,也可以为 return right
# 递归操作
if left < right:
pivotIndex = partition(nums, left, right)
quickSort2(nums, left, pivotIndex - 1) # 左序列
quickSort2(nums, pivotIndex + 1, right) # 右序列
return nums
插入排序如同打扑克一样,每次将后面的牌插到前面已经排好序的牌中。插入排序有一种优化算法,叫做拆半插入。因为前面是局部排好的序列,因此可以用二分查找的方法将牌插入到正确的位置,而不是从后往前一一比对。折半查找只是减少了比较次数,但是元素的移动次数不变,所以时间复杂度仍为 O(n^2) !
def insertionSort(nums):
for i in range(len(nums) - 1): # 遍历 len(nums)-1 次
curNum, preIndex = nums[i+1], i # curNum 保存当前待插入的数
while preIndex >= 0 and curNum < nums[preIndex]: # 将比 curNum 大的元素向后移动
nums[preIndex + 1] = nums[preIndex]
preIndex -= 1
nums[preIndex + 1] = curNum # 待插入的数的正确位置
return nums
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。希尔排序的核心在于增量序列的设定。既可以提前设定好增量序列,也可以动态的定义增量序列。希尔排序的性能分析很是复杂,取决于增量序列的选取。由于最后一个增量必须是1,那么增量的选取可以是 。最后分析出来,其时间复杂度为 O(n^1.3),总之比直接插入排序快很多啦。
这种排序方法应用较少,了解一下即可。
def shellSort(nums):
lens = len(nums)
gap = 1
gap //= 2 # 增量
while gap > 0:
for i in range(gap, lens):
curNum, preIndex = nums[i], i - gap # curNum 保存当前待插入的数
while preIndex >= 0 and curNum < nums[preIndex]:
nums[preIndex + gap] = nums[preIndex] # 将比 curNum 大的元素向后移动
preIndex -= gap
nums[preIndex + gap] = curNum # 待插入的数的正确位置
gap //= 2 # 下一个间隔
return nums
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。选择排序每次选出最小的元素,因此需要遍历 n-1 次。实在是太暴力无脑了。
工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
可以看出,选择排序中每趟总是从无序区中选择选出全局最小(最大)的关键字,所以,直接选择排序和堆排序适合于从大量记录中选择一部分排序记录。
def selectionSort(nums):
for i in range(len(nums) - 1): # 遍历 len(nums)-1 次
minIndex = i
for j in range(i + 1, len(nums)):
if nums[j] < nums[minIndex]: # 更新最小值索引
minIndex = j
nums[i], nums[minIndex] = nums[minIndex], nums[i] # 把最小数交换到前面
return nums
堆排序是指利用堆这种数据结构所设计的一种排序算法。鉴于之前已学习了堆,理解起来很方便。不过堆排序属于选择排序,这是如何理解呢?我们进行堆排序时,是in-place操作的,我们还是要关注一下在原数组中,进行堆排序的原理。
堆排序思想:将待排序的序列构造成一个大顶堆,然后依次将堆顶元素移走并重新调整剩余的n-1个元素为大顶堆,与直接选择排序很类似。
堆排序的关键是构造初始堆,将数组看成是一棵完全二叉树的顺序存储结构,从 大者上浮,小者筛选下去,此时根结点为最大值,将其放到数组最后,即与最后一个叶子结点交换。由于最大元素归位,待排序的元素个数减少一个;如此反复建堆。
# 最大堆
def heapSort(nums):
# 调整堆
def adjustHeap(nums, i, size):
# 非叶子结点的左右两个孩子
lchild = 2 * i + 1
rchild = 2 * i + 2
# 在当前结点、左孩子、右孩子中找到最大元素的索引
largest = i
if lchild < size and nums[lchild] > nums[largest]:
largest = lchild
if rchild < size and nums[rchild] > nums[largest]:
largest = rchild
# 如果最大元素的索引不是当前结点,把大的结点交换到上面,继续调整堆
if largest != i:
nums[largest], nums[i] = nums[i], nums[largest]
# 第 2 个参数传入 largest 的索引是交换前大数字对应的索引
# 交换后该索引对应的是小数字,应该把该小数字向下调整
adjustHeap(nums, largest, size)
# 建立堆
def builtHeap(nums, size):
for i in range(len(nums)//2)[::-1]: # 从倒数第一个非叶子结点开始建立最大堆
adjustHeap(nums, i, size) # 对所有非叶子结点进行堆的调整
# print(nums) # 第一次建立好的最大堆
# 堆排序
size = len(nums)
builtHeap(nums, size)
for i in range(len(nums))[::-1]:
# 每次根结点都是最大的数,最大数放到后面
nums[0], nums[i] = nums[i], nums[0]
# 交换完后还需要继续调整堆,只需调整根节点,此时数组的 size 不包括已经排序好的数
adjustHeap(nums, 0, i)
return nums # 由于每次大的都会放到后面,因此最后的 nums 是从小到大排列