排序算法
介绍
本节内容将介绍几种常见的排序算法(主要包含冒泡排序,选择排序,插入排序,希尔排序,归并排序,快速排序,堆排序,桶排序)的算法原理,算法复杂度的分析,以及如何实现。
知识点
- 冒泡排序
- 选择排序
- 插入排序
- 希尔排序
- 归并排序
- 快速排序
- 堆排序
- 计数排序
- 桶排序
排序算法
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。
注:定义来自百度百科。
在学习算法的时候,需要学会理解算法是如何实现的,掌握其算法的原理,以及如何判断算法的优越性。
我们将在接下来的内容,逐步理解这些简单的搜索算法。
冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法原理
-
比较相邻的元素。如果第一个比第二个大,就交换他们
-
对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数
-
针对所有的元素重复以上步骤,除了最后一个
-
重复步骤 1~3,直到排序完成
复杂度分析
最坏复杂度: 时间复杂度为 O(n^2)
最好复杂度:时间复杂度为 O(n)
平均复杂度: 时间复杂度为 O(n^2)
实现
def bubble_sort(sequence):
for i in range(1,len(sequence)):
for j in range(0,len(sequence)-i):
if sequence[j]>sequence[j+1]:
sequence[j],sequence[j+1]=sequence[j+1],sequence[j]
return sequence
参考代码
注意:请务必自己独立思考解决问题之后再对照参考答案,一开始直接看参考答案收获不大。
def bubble_sort(sequence):
for i in range(1,len(sequence)):
for j in range(0,len(sequence)-i):
if sequence[j]>sequence[j+1]:
sequence[j],sequence[j+1]=sequence[j+1],sequence[j]
return sequence
if __name__ == '__main__':
sequence= [12, 27, 46, 16, 25, 37, 22, 29, 15, 47, 48, 34]
print(sequence)
print(bubble_sort(sequence))
选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n^2) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧
算法原理
-
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
-
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾
-
重复第二步,直到所有元素均排序完毕
复杂度分析
最坏复杂度: 时间复杂度为 O(n^2)
最好复杂度:时间复杂度为 O(n^2)
平均复杂度: 时间复杂度为 O(n^2)
实现
def select_sort(sequence):
for i in range(len(sequence)-1):
minIndex=i #记录最小数的索引
for j in range(i+1,len(sequence)):
if sequence[j]<sequence[minIndex]:
minIndex=j
sequence[minIndex],sequence[i]=sequence[i],sequence[minIndex] #将该数放到已排序序列的末尾
return sequence
插入排序
插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法原理
-
从第一个元素开始,该元素可以认为已经被排序
-
取出下一个元素,在已经排序的元素序列中从后向前扫描
-
如果该元素(已排序)大于新元素,将该元素移到下一位置
-
重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置
-
将新元素插入到该位置后
-
重复步骤 2-5
复杂度分析
最坏复杂度: 时间复杂度为 O(n^2)
最好复杂度:时间复杂度为 O(n^2)
平均复杂度: 时间复杂度为 O(n^2)
实现
def insertion_sort(sequence):
for index in range(1,len(sequence)):
while(index>0 and sequence[index-1]>sequence[index]):
sequence[index],sequence[index-1]=sequence[index-1],sequence[index]
index=index-1
return sequence
希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
-
插入排序在对几乎已经排好序的数据操作时,效率高,既可以达到线性排序的效率
-
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
算法原理
基尔排序基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
-
选择一个增量序列 t1,t2,…,tk,其中 ti>tj,tk=1
-
按增量序列的个数 k,对序列进行 k 趟排序
-
每趟排序,根据对应的增量 ti,将待排序序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
复杂度分析
最坏复杂度: 时间复杂度为 O(n(logn)^2)
最好复杂度:时间复杂度为 O(nlogn)
平均复杂度: 取决于间隔序列
实现
def shell_sort(sequence):
gap=len(sequence)
while gap >1:
gap=gap//2
for i in range(gap,len(sequence)):
for j in range(i%gap,i,gap):
if sequence[i]<sequence[j]:
sequence[i],sequence[j]=sequence[j],sequence[i]
return sequence
归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为 2-路归并
算法原理
归并排序基本思想是:
-
把长度为 n 的输入序列分成两个长度为 n/2 的子序列
-
对这两个子序列分别采用归并排序
-
将两个排序好的子序列合并成一个最终的排序序列
复杂度分析
最坏复杂度: 时间复杂度为 O(nlogn)
最好复杂度:时间复杂度为 O(nlogn)
平均复杂度: 时间复杂度为 O(nlogn)
实现
import math
def merge_sort(sequence):
if(len(sequence)<2):
return sequence
mid=math.floor(len(sequence)/2)
left,right=sequence[0:mid],sequence[mid:]
return merge(merge_sort(left),merge_sort(right))
def merge(left,right):
result=[]
while left and right:
if left[0] <=right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
while left:
result.append(left.pop(0))
while right:
result.append(right.pop(0))
return result
快速排序
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
算法原理
归并排序基本思想是:使用分治法来把一个串(list)分为两个子串(sub-lists)
-
从数列中挑出一个元素,称为"基准"(pivot)
-
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
-
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
复杂度分析
最坏复杂度: 时间复杂度为 O(n^2)
最好复杂度:时间复杂度在 O(n) 和 O(nlogn)中间
平均复杂度: 时间复杂度为 O(nlogn)
实现
def quick_select(sequence):
def recursive(begin,end):
if begin>end:
return
left,right=begin,end
pivot=sequence[left]
while left < right:
while left<right and sequence[right]>pivot:
right=right-1
while left<right and sequence[left]<=pivot:
left=left+1
sequence[left], sequence[right] = sequence[right], sequence[left]
sequence[left], sequence[begin] = pivot, sequence[left]
recursive(begin, left - 1)
recursive(right + 1, end)
recursive(0, len(sequence) - 1)
return sequence
堆排序
堆排序(Heapsort)的基本思想:是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
算法原理
堆排序基本思想是:
-
将初始待排序关键字序列(R1,R2…Rn)构建成大顶堆,此堆为初始的无序区。
-
将堆顶元素 R1与最后一个元素 R[n]交换,此时得到新的无序区(R1,R2,…Rn-1)和新的有序区(Rn),且满足 R[1,2…n-1]<=R[n]。
-
由于交换后新的堆顶 R1可能违反堆的性质,因此需要对当前无序区(R1,R2,…Rn-1)调整为新堆,然后再次将 R1与无序区最后一个元素交换,得到新的无序区(R1,R2…Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为 n-1,则整个排序过程完成。
复杂度分析
最坏复杂度: 时间复杂度为 O(nlogn)
最好复杂度:时间复杂度在 O(nlogn)
平均复杂度: 时间复杂度为 O(nlogn)
实现
import random
def heap_sort(sequence):
def heap_adjust(parent):
child = 2 * parent + 1 # left child
while child < len(heap):
if child + 1 < len(heap):
if heap[child + 1] > heap[child]:
child =child+ 1 # right child
if heap[parent] >= heap[child]:
break
heap[parent], heap[child] = \
heap[child], heap[parent]
parent, child = child, 2 * child + 1
heap, sequence = sequence.copy(), []
for i in range(len(heap) // 2, -1, -1):
heap_adjust(i)
while len(heap) != 0:
heap[0], heap[-1] = heap[-1], heap[0]
sequence.insert(0, heap.pop())
heap_adjust(0)
return sequence
if __name__ == '__main__':
sequence= [random.randint(1,10000) for i in range(50)]
print(sequence)
print(heap_sort(sequence))
计数排序
计数排序(Counting Sort)不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数
算法原理
计数排序基本思想是:
-
找出待排序的数组中最大和最小的元素
-
统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项
-
对所有的计数累加(从 C 中的第一个元素开始,每一项和前一项相加)
-
反向填充目标数组:将每个元素 i 放在新数组的第 C(i)项,每放一个元素就将 C(i)减去 1
复杂度分析
最坏复杂度: 时间复杂度为 O(n+k)
最好复杂度:时间复杂度在 O(n+k)
平均复杂度: 时间复杂度为 O(n+k)
实现
import random
def counting_sort(sequence):
if sequence == []:
return []
sequence_len=len(sequence)
sequence_max=max(sequence) #找出待排序的数组中最大的元素
sequence_min=min(sequence) #找出待排序的数组中最小的元素
counting_arr_length=sequence_max-sequence_min+1
counting_arr=[0]*counting_arr_length
for number in sequence:
counting_arr[number-sequence_min]+=1 #统计数组中元素出现次数
for i in range(1,counting_arr_length): #计数累加
counting_arr[i]=counting_arr[i]+counting_arr[i-1]
ordered=[0]*sequence_len
for i in range(sequence_len-1,0,-1): #反向填充目标数组
ordered[counting_arr[sequence[i]-sequence_min]-1]=sequence[i]
counting_arr[sequence[i]-sequence_min]-=1
return ordered
if __name__ == '__main__':
sequence=[random.randint(1,10000) for i in range(500)]
print(sequence)
print(counting_sort(sequence))
桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)
算法原理
计数排序基本思想是:
-
设置一个定量的数组当作空桶
-
遍历输入数据,并且把数据一个一个放到对应的桶里去
-
对每个不是空的桶进行排序
-
从不是空的桶里把排好序的数据拼接起来
复杂度分析
最坏复杂度: 时间复杂度为 O(n+k)
最好复杂度:时间复杂度在 O(n)
平均复杂度: 时间复杂度为 O(n)
实现
import math
import random
DEFAULT_BUCKET_SIZE = 5
def insertion_sort(sequence):
for index in range(1,len(sequence)):
while(index>0 and sequence[index-1]>sequence[index]):
sequence[index],sequence[index-1]=sequence[index-1],sequence[index]
index=index-1
return sequence
def bucket_sort(sequence, bucketSize=DEFAULT_BUCKET_SIZE):
if(len(sequence) == 0):
return []
minValue = sequence[0]
maxValue = sequence[0]
for i in range(0, len(sequence)):
if sequence[i] < minValue:
minValue = sequence[i] #寻找最小值
elif sequence[i] > maxValue:
maxValue = sequence[i] #寻找最大值
bucketCount = math.floor((maxValue - minValue) / bucketSize) + 1 #桶的初始化
buckets = []
for i in range(0, bucketCount):
buckets.append([])
for i in range(0, len(sequence)): #遍历数据,将数据依次放进桶中
buckets[math.floor((sequence[i] - minValue) / bucketSize)].append(sequence[i])
sortedArray = []
for i in range(0, len(buckets)): #将每一个不是空的桶进行排序
insertion_sort(buckets[i])
for j in range(0, len(buckets[i])):
sortedArray.append(buckets[i][j])
return sortedArray
if __name__ == '__main__':
sequence=[random.randint(1,10000) for i in range(50)]
print(sequence)
print(bucket_sort(sequence))
总结
排序算法在计算机应用中,排序是常用的基本运算,尤其是在大量数据的处理方面。本节内容依次介绍几种常见排序算法的算法原理和实现过程,以及对应的复杂度分析。回顾下本节内容主要包含了以下内容:
- 冒泡排序
- 选择排序
- 插入排序
- 希尔排序
- 归并排序
- 快速排序
- 堆排序
- 计数排序
- 桶排序
几种常见排序算法复杂度总结如下:
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(n^2) | O(n) | O(1) | 不稳定 |
归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 |
快速排序 | O(nlog2n) | O(n^2) | O(nlog2n) | O(nlog2n) | 不稳定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 |
桶排序 | O(n+k) | O(n^2) | O(n) | O(n+k) | 稳定 |
请把文档中所有的示例代码都手动完成并运行对比结果,只有这样才能更加熟练的掌握对应的相关知识。