一.引言
本博客大致会提及到排序算法的理论讲解,但是不会深入讲解,强烈推荐数据结构和算法的可视化网站。本文给出八种经典的排序算法的Python实现代码和部分注解,算是一个总结,也感谢网络众多优秀的博主分享他们的idea,站在巨人的肩膀上果然成长很迅速。排序可以说在经典算法中是很重要的一部分,对于常见的排序算法要做到本能的敲出代码,了解各种算法的时间复杂度和空间复杂度,关于时间复杂度和空间复杂度具体可以参考下面的图表。对于外部排序和线性时间的排序也请参考reference的链接。对于稳定性的判断,请记住口诀,不稳定:快选堆希 稳定:插冒归基。
数据结构与算法-排序可视化网址:https://visualgo.net/zh/sorting
关于排序过程中的操作基础的可以分为三大类。
插入排序类 | 选择排序类 | 交换排序类 | |||
直接插入排序 | 希尔排序 | 简单选择排序 | 堆排序 | 冒泡排序 | 快速排序 |
二.时间复杂度和空间复杂度
三.排序算法的选择
如果我们的待排数据基本有序,我们应该从最好的情况来考虑,所以我们应该选择:插入排序和冒泡排序
如果我们的待排数据非常不规则,我们应该从最坏情况考虑,应该选择:堆排序和归并排序
从时间复杂度考虑最稳定的排序算法属于:堆排序和归并排序都是O(nlogn)
如果在乎内存空间考虑,归并的空间复杂度为O(n)上图有错误,此时归并和快排不是最好的选择。
如果在乎稳定性首选归并排序
如果排序记录少,插入排序是很好的。
如果排序数据很多,考虑时间复杂度的话,我们优先选择快速排序。
比较重要的三个排序:快速排序,归并排序和桶排序。
四.思想和代码实现
下面的代码均经过测试都可以使用,全都是从小到大排序的
1冒泡排序:
冒泡排序是相邻的两个进行比较符合条件就进行交换,经过一次比较后,就会把最大的元素放在最后面
所以在这里循环条件是从 i 是从0到length的
而j是从0到length-i,的因为我们经过一次比较就少一个元素
#冒泡排序:双重for循环
from collections import deque
def bubbleSort(nums):
if not isinstance(nums,list) or not nums:
return -1
for i in range(1,len(nums)):
#在这里有len(nums)-i是因为最后有排号的元素
for j in range(0,len(nums)-i):
if nums[j]>nums[j+1]:
nums[j],nums[j+1]=nums[j+1],nums[j]
return nums
2选择排序:
选择排序中,我们需要用一个变量来储存最大值(最小值),时间复杂度都是O(n2),因为我们也要选择n次
在这里我们的循环控制变量 i 是从0到length-1
我们每次选出最小的放在第一位,所以j是从i+1到length
#选择排序 需要使用一个变量,来储存最大值
def selectSort(nums):
if not isinstance(nums,list) or not nums:
return -1
#由于在这里是从头开始的,所以需要把最小的数往前移动
#每次也只是索引一个数字
for i in range(0,len(nums)-1):
min_index=i
for j in range(i+1,len(nums)):
if nums[j]<nums[min_index]:
min_index=j#索引最小值
if min_index!=i:
nums[i],nums[min_index]=nums[min_index],nums[i]
return nums
3.插入排序:
在插入排序中,分为了两个部分,待排序部分(或者称之为待插入部分),和已排序部分
在数据中,前半部分是已排序部分,后半部分是待插入部分
在这里,i负责全局的循环次数
而 j则是控制循环的数据,在这里面进行两两比较和交换
#插入排序是从前往后排,默认前面的已经是有序的了
def insertSort(nums):
if not isinstance(nums,list) or not nums:
return -1
for i in range(1,len(nums)):
for j in range(i,0,-1):#在这里会进行不停地交换,所以不用担心
if nums[j]<nums[j-1]:
nums[j],nums[j-1]=nums[j-1],nums[j]
else:
break
return nums
4.快速排序:
快速排序中,我们需要找到一个轴值pivot,然后把小于pivot的值放在左边,大于pivot的值放在右边
然后里面使用递归进行实现,再对左右两边的数据进行排序
这里的终止条件是left==right
非递归实现
def quick_sort(arr):
'''''
模拟栈操作实现非递归的快速排序
'''
if len(arr) < 2:
return arr
stack = []
stack.append(len(arr)-1)
stack.append(0)
while stack:
l = stack.pop()
r = stack.pop()
index = partition(arr, l, r)
if l < index - 1:
stack.append(index - 1)
stack.append(l)
if r > index + 1:
stack.append(r)
stack.append(index + 1)
def partition(arr, start, end):
# 分区操作,返回基准线下标
pivot = arr[start]
while start < end:
while start < end and arr[end] >= pivot:
end -= 1
arr[start] = arr[end]
while start < end and arr[start] <= pivot:
start += 1
arr[end] = arr[start]
# 此时start = end
arr[start] = pivot
return start
递归实现
def quick_sort1(nums,start,end):
if start>=end:
return
pivot=nums[start]
left=start
right=end
while left<right:#在这里有个比较的过程,其实双层for循环
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[right]=pivot#只是轴心值调整而已
quick_sort1(nums,start,left-1)
quick_sort1(nums,left+1,end)
return nums
def quick_sort2(array):
if len(array) < 2:
return array
else:
array1=quick_sort2([item for item in array[1:] if item <= array[0]]) + [array[0]] + quick_sort2([item for item in array[1:] if item > array[0]])
return array1
#一句话快排
quick_sort = lambda array: array if len(array) <= 1 else quick_sort([item for item in array[1:] if item <= array[0]]) + [array[0]] + quick_sort([item for item in array[1:] if item > array[0]])
5.堆排序:
堆排序过程有两个,我们需要写一个调整堆的函数,在这里我们调整为大顶堆
这样我们得到的堆顶元素就是最大值
然后我们要取出最大值,则只要与堆数组的最后一个元素进行交换即可,这样我们相当于在选择最大的数放在了数组的最后面,然后我们把剩下的元素再进行堆调整即可
堆的特点:父节点是i,则做孩子是2i,右孩子是2i+1
我们调整堆的过程是从最底部开始进行调整,再往后进行不断的调整
步骤:调整堆,把最大元素移到堆最上面,再把他移到数组最后面,再调整堆
#将待排的序列构成一个大顶堆,最大值在上面 1.如何由一个无序数列构建成一个堆2.在输出堆顶元素后,如何调整成一个新的堆,没有递归的
def heap_sort(nums):
length=len(nums)-1
first_sort_count=length//2
#把序列调整为大顶堆
for i in range(first_sort_count):
heap_adjust(nums,first_sort_count-i,length)
#堆末尾的元素与堆顶进行交换,再把剩下的元素重新调整,为什么是length-1,指的是剩下的元素
for i in range(length-1):
nums=swap(nums,1,length-i)
heap_adjust(nums,1,length-i-1)
return [nums[i] for i in range(1,len(nums))]
def swap(nums,i,j):
nums[i],nums[j]=nums[j],nums[i]
return nums
#如何进行堆的调整,从底端开始
def heap_adjust(nums,start,end):#目标把最大的值交换到根节点上
temp=nums[start]
i=start#子树中的父节点
j=2*i
while j<=end:#在调整完整棵树前一直循环
if (j<end) and (nums[j]<nums[j+1]):#要取得最大的左子树的坐标
j+=1
if temp<nums[j]:#要做的事如果根节点小于树结点,就进行交换
nums[i]=nums[j]
#这个是为了在中间部分结点时候的操作
i=j#然后再往下走,所以才有这么两句话,这个是为了往上一层走
j=2*i
else:
break
nums[i]=temp#循环完以后i变成最后一个元素
6.希尔排序:
希尔排序算法是一个不稳定的算法,它采用一个步长控制gap,它会把数组分成gap份,假如gap为5,则数组会被分成5份,有点分治算法的意思,然后在每个小数组中调用插入排序
循环的终止条件是gap=0(此时也就是有序了)
#希尔排序,需要使用一个gap,来进行对数组步长的分解,同时停止排序的终止条件是gap=1,相当于插入排序
def shell_sort(nums):
if not isinstance(nums,list) or not nums:
return -1
length=len(nums)
gap=length//2
while gap>=1:
#这才是一个插入循环,因为插入排序在基本有序时很高效
for j in range(gap,length):#这一个是用来控制第几组循环的,相当于去了中间那个数往后走
i=j
#增量的插入排序
while (i-gap)>=0:#这个是用来控制组内的第几个数的交换的
if nums[i]<nums[i-gap]:
nums[i],nums[i-gap]=nums[i-gap],nums[i]
i-=gap
else:
break
gap//=2
return nums
7.归并排序:
归并排序的思想是把数组不断往下进行二分,然后再两两进行合并往上进行排序
所以一共有两个函数,一个是用来合并两个数组的
一个是用来左右两边数据的
这里面采用了递归的思想
#归并排序
def merge_sort(nums):
if not isinstance(nums,list) or not nums:#看这里是否是list变量
return -1
n=len(nums)
if n==1:
return nums
mid=n//2
left_list=merge_sort(nums[:mid])
right_list=merge_sort(nums[mid:])
return merge(left_list,right_list)
def merge(left_list,right_list):
left=0
right=0
merge_result=[]
while left<len(left_list) and right<len(right_list):
if left_list[left]<=right_list[right]:
merge_result.append(left_list[left])
left+=1
else:
merge_result.append(right_list[right])
right+=1
merge_result+=left_list[left:]
merge_result+=right_list[right:]
return merge_result
8.计数排序:
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
def countingSort(nums, maxValue):
bucketLen = maxValue+1
bucket = [0]*bucketLen
sortedIndex =0
numsLen = len(nums)
for i in range(numsLen):#在这里构建计数规则
if not bucket[nums[i]]:
bucket[nums[i]]=0
bucket[nums[i]]+=1
for j in range(bucketLen):
while bucket[j]>0:
nums[sortedIndex] = j
sortedIndex+=1
bucket[j]-=1
return nums
9.桶排序:
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
1. 什么时候最快
当输入的数据可以均匀的分配到每一个桶中。
2. 什么时候最慢
当输入的数据被分配到了同一个桶中。
def bucketSort(nums):
bucket=[0]*(max(nums)-min(nums)+1)
for i in range(len(nums)):
bucket[nums[i]-min(nums)]+=1
tmp=[]
for i in range(len(bucket)):
if bucket[i]!=0:
tmp+=[min(nums)+i]*bucket[i]
return tmp
10.基数排序:
基数排序是桶排序的扩展,它是按照位数切割成不同的数字,然后按每个位数进行比较,所以一共会有十个桶从0到9
具体做法:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
从低位开始往高位进行排序
所以我们得先求出一共有多少位,然后分别再放到桶里去
这里得循环条件是位数,有多少位循环多少次
#基数排序
def radix_sort(nums):
max_number=max(nums)
n=1
while max_number>=10**n:#在这里要设置为等于号,因为我们在后面无法取到n
n+=1
#产生0~9之间的10个桶
for k in range(n):#k在这里是控制位数的
buckets=[[] for i in range(10)]#桶要在这里产生
for subnum in nums:
buckets[int(subnum/(10**k)%10)].append(subnum)#这里就控制了个位十位等
nums=[num for bucket in buckets for num in bucket]
return nums