排序算法有非常多,应用也非常多,在各种笔试面试中也常常出现,所以现在就来复习一下相关的排序算法吧!
下面会介绍多种排序算法,在此之前先说一下,排序算法的评价主要有以下几个方面:
- 排序算法的时间复杂度;
- 排序算法的空间复杂度;
- 排序算法的稳定性
其中前两个是老生常谈了,基本提到算法都会考虑这两点。第三点中排序算法的稳定性是指如果待排序列中存在相同元素时,经过排序之后相同元素的先后顺序是否被打乱,如果保持不变则说明这个排序算法是稳定的,否则称该排序算法是不稳定的。
1.快速排序(quickSort)
快速排序是最常见的排序算法之一,需要重点掌握,要能够手撕代码的程度。
快速排序其实用了分而治之 的策略,它的一个核心思想就是:选择一个枢纽(pivot),通过元素交换使得比pivot小的元素在它左边,比它大的元素在它右边,再分别对左右两边做相同的操作。
快速排序的执行时间与数据序列的初始排列和基准值选取有关,
最好的情况下 每次选择的主元都能够正中的划分序列,此时的时间复杂度是T(N)=O(N*logN),
在最坏的情况下 ,每次选择的主元都是极值的话,时间复杂度会达到T(N)=O(N^2)。(这个可以想象,如果每次主元都是最大值的话,那么划分相当于无意义,每次都要将剩余的所有元素进行排序,这样起不到快速排序的效果)
快排的实现过程中有两个重要的步骤:
- 选主元(pivot)
选主元为何重要,根据前面提到的快排的时间复杂度就可以知道,选取主元会非常大程度影响快排的效率。
一般情况下,最经典的选取主元方式就是任意头/中/尾的其中一个,这是最方便的做法。但是如果遇到本身已经有序的极端情况,选择头或尾作为主元就会非常危险。
那么也可以选择取头/中/尾的中位数作为主元(mid_of_3)。
或者可以随机选择一个未排序序列中的数作为主元,这也是有效的降低取到极值概率的办法。
#设有一个列表nums,列表长度len
#随机选择主元
import random
random_index=random.randint(0,len-1)
pivot=nums[random_index]
- 分割(partition)
分割是快排实现中最核心的部分,它最主要的过程就是将比pivot小的元素放到pivot左边,比pivot大的元素放到pivot右边。这里用双指针来实现:
def partition(self,nums,low,high):
pivot = nums[high] #pivot选取可参考第一点
i, j = low, high #选取i、j两个指针,分别从头、尾开始
while i < j:
while i < j and nums[j] >= pivot:
j -= 1 #当j所在的元素比pivot大时(说明不用移动),j往左移
while i < j and nums[i] <= pivot:
i += 1 #当i所在的元素比pivot小时(说明不用移动),i往右移
if i < j :
nums[i], nums[j] = nums[j], nums[i] #当跳转前两个while时,说明i和j所在位置都不符合快排要求,则对这两个元素进行交换
nums[i], nums[low] = nums[low], nums[i] #把pivot放到i所在的位置
return i
2.冒泡排序(bubbleSort)
冒泡排序的思想是很简单的,最主要的两个点是:比较相邻元素,大的元素后置(在升序情况下)。这样每一次都可以将最大的元素放到最后面,排序每进行一趟可以确定一个元素的位置。
正常情况下,冒泡排序的平均时间复杂度是O(n2),因为每一趟都要遍历n-t个元素(t为趟数)。
当然我们可以设计一个标识符,用于标识元素是否有进行交换,当一趟排序之后所有元素都没有进行交换时,说明序列已经排序完成。那么此时冒泡排序的时间复杂度与初始待排序列有关。当初始序列就是已排序序列时,冒泡排序的最好时间复趟杂度是O(n)(第一没有任何元素进行交换,结束排序),当初始序列是倒序排列时,每一趟都要交换所有元素,此时它的时间复杂度为O(n2)。
冒泡排序中我们只是进行了元素自身的交换,并没有利用额外空间,因此空间复杂度为O(1)。
由于在元素相等时,我们并不会进行交换,所以冒泡排序是稳定的。
算法实现:
"""count用来标记元素是否进行了交换"""
def bubleSort(self, nums):
n = len(nums)
if n <= 1:
return
for i in range(n):
count = 0
for j in range(n - i - 1):
if nums[j] > nums[j + 1]:
count += 1
nums[j], nums[j + 1] = nums[j + 1], nums[j]
if count == 0:
break
return nums
3.插入排序(insertionSort)
插入排序将序列分成待排序列和已排序列,核心是每次将待排序列中的一个元素在已排序列中找到正确的位置插入。
时间复杂度来说,最好的情况下,如果序列原本就是排序的,此时时间复杂度为O(n),最坏的情况下,如果序列是倒序的,此时的时间复杂度O(n2),平均时间复杂度为O(n2)。
插入排序是最稳定的排序算法。
二分插入排序——利用二分查找来找到元素正确的插入位置,这时候查找的时间复杂度可以将为O(logn)。
算法实现
def insertionSort(self, nums):
# 待插入的元素,curr
for i in range(1, len(nums)):
curr = nums[i]
for j in range(i-1, -1, -1): # 比curr大的元素后移
if nums[j] > curr:
nums[j+1] = nums[j]
else:
j = j+1
break
nums[j] = curr
return nums
4.选择排序(selectionSort)
选择排序把序列分成两部分,已排序列和待排序列。
在每一趟排序中,我们在待排序列中选择最小元素,放到已排序列的末尾。
选择排序和插入排序区别在于,插入排序是对已排序序列进行操作,找到元素合适的位置,选择排序则是对未排序序列进行操作,找到合适的元素。
选择排序我们一样可以看到,它每一趟都需要遍历待排序列找到最小元素,因此它的时间复杂度为O(n2)。
由于也是在原地交换,因此空间复杂度为O(1)。
选择排序是不稳定的排序算法。
算法实现:
def selectSort(self, nums):
tmp_sorted = 0 # 用于标记已排序列的下一个位置
min_index = 0 # 用于标记待排序列中最小值的位置
for k in range(len(nums)):
for i in range(tmp_sorted, len(nums)): # 这个循环是在待排序列中找到最小值
if nums[min_index] > nums[i]:
min_index = i
# 将最小值放到已排序列尾部
nums[tmp_sorted], nums[min_index] = nums[min_index], nums[tmp_sorted]
tmp_sorted += 1
min_index = tmp_sorted
return nums
5.归并排序(mergeSort)
归并排序的过程是先将待排序列不断切分直到只剩一个元素。然后再依次对相邻的两组元素进行合并(merge)。
归并排序无论在任何情况下都是要先划分序列再归并,因此时间复杂度都是O(nlogn),空间复杂度由于递归要调用栈,因此空间复杂度为O(n)。
**对于归并排序,当结点数为偶数时,中间结点建议选左边结点
算法实现:
# 用于合并两个有序子序列
def merge(self, left, right):
res = []
tmp_left, tmp_right = 0, 0
while tmp_left < len(left) and tmp_right < len(right):
if left[tmp_left] < right[tmp_right]:
res.append(left[tmp_left])
tmp_left += 1
else:
res.append(right[tmp_right])
tmp_right += 1
# 当其中一个序列为空时就将另一个序列元素直接添加到res末尾
if tmp_left < len(left):
res += left[tmp_left:]
if tmp_right < len(right):
res += right[tmp_right:]
return res
def merge_sort(self, nums):
if len(nums) <= 1:
return nums
mid = len(nums)//2
# 不断切分原始序列直至只剩一个元素
left = self.merge_sort(nums[:mid])
right = self.merge_sort(nums[mid:])
return self.merge(left, right)
6.桶排序(bucketSort)
后续补充