排序算法汇总
在笔试面试的过程当中,经常会考察一下常见的几种排序算法,包括冒泡排序,选择排序,插入排序,希尔排序,快速排序,堆排序归并排序等7种排序算法,下面将分别进行讲解:css
1.冒泡排序
所谓冒泡排序法,就是对一组数字进行从大到小或者从小到大排序的一种算法。具体方法是,相邻数值两两交换。从第一个数值开始,若是相邻两个数的排列顺序与咱们的指望不一样,则将两个数的位置进行交换(对调);若是其与咱们的指望一致,则不用交换。重复这样的过程,一直到最后没有数值须要交换,则排序完成。通常地,若是有N个数须要排序,则须要进行(N-1)趟起泡。html
冒泡排序是最简单的排序之一了,其大致思想就是经过与相邻元素的比较和交换来把小的数交换到最前面。这个过程相似于水泡向上升同样,所以而得名。举个栗子,对5,3,8,6,4这个无序序列进行冒泡排序。首先从后向前冒泡,4和6比较,把4交换到前面,序列变成5,3,8,4,6。同理4和8交换,变成5,3,4,8,6,3和4无需交换。5和3交换,变成3,5,4,8,6,3.这样一次冒泡就完了,把最小的数3排到最前面了。对剩下的序列依次冒泡就会获得一个有序序列。冒泡排序的时间复杂度为O(n^2)。python
-
def BubbleSort(A):
#冒泡,一趟最小排最前,N-1趟
-
if A ==
None
or len(A) ==
0:
-
return
-
for i
in range(len(A)):
-
for j
in range(len(A)
-1, i,
-1):
-
if A[j] < A[j
-1]:
-
tmp = A[j
-1]
#Swap()
-
A[j
-1] = A[j]
-
A[j] = tmp
-
#print(A)
-
return A
冒泡排序的改进:面试
在冒泡排序中,经过先后2个数据的两两交换,来完成排序过程,而若是某一趟并无发生交换,说明此时序列已经有序,就能够终止排序过程。算法
-
def BubbleSort2(A):
-
flag =
True
-
for i
in range(len(A)):
-
if flag:
#为真时才执行一趟
-
for j
in range(len(A)
-1, i,
-1):
-
flag =
False
-
if A[j] < A[j
-1]:
-
tmp = A[j
-1]
#Swap()
-
A[j
-1] = A[j]
-
A[j] = tmp
-
flag =
True
#交换
-
#print(A)#看比较的次数
-
return A
2.选择排序
选择排序简单的说就是每次找到序列中的最小值,而后将该值放在有序序列的最后一个位置,以造成一个更大的有序序列。选择排序进行n趟,每趟从i+1开始,每趟找到最小值下标min,再将a[min]与a[i]交换。swift
选择排序的思想其实和冒泡排序有点相似,都是在一次排序后把最小的元素放到最前面。可是过程不一样,冒泡排序是经过相邻的比较和交换。而选择排序是经过对总体的选择。举个栗子,对5,3,8,6,4这个无序序列进行简单选择排序,首先要选择5之外的最小数来和5交换,也就是选择3和5交换,一次排序后就变成了3,5,8,6,4.对剩下的序列一次进行选择和交换,最终就会获得一个有序序列。其实选择排序能够当作冒泡排序的优化,由于其目的相同,只是选择排序只有在肯定了最小数的前提下才进行交换,大大减小了交换的次数。选择排序的时间复杂度为O(n^2)。数组
-
def
SelectSort(
A):
-
for i
in range(len(
A)):
-
min = i#最小值所在的位置,最小放最前
-
for j
in range(i+
1, len(
A)):
-
if
A[
min] >
A[j]:
-
min = j
-
if
min != i:
-
tmp =
A[
min]
-
A[
min] =
A[i]
-
A[i] = tmp
-
return
A
3.插入排序
插入排序能够简单归纳为:假定序列下标i以前数据是有序的,则从i-1位置数据开始,依次将其与i进行比较并交换(当该值不知足插入条件,即该位置值大于i位置值时),最终找到一个合适的位置插入下标i数据,以造成一个更大的有序序列。 ruby
插入排序不是经过交换位置而是经过比较找到合适的位置插入元素来达到排序的目的的。相信你们都有过打扑克牌的经历,特别是牌数较大的。在分牌时可能要整理本身的牌,牌多的时候怎么整理呢?就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是同样的。举个栗子,对5,3,8,6,4这个无序序列进行简单插入排序,首先假设第一个数的位置时正确的,想一下在拿到第一张牌的时候,不必整理。而后3要插到5前面,把5后移一位,变成3,5,8,6,4.想一下整理牌的时候应该也是这样吧。而后8不用动,6插在8前面,8后移一位,4插在5前面,从5开始都向后移一位。注意在插入一个数的时候要保证这个数前面的数已经有序。简单插入排序的时间复杂度也是O(n^2)。函数
-
def InsertSort(A):
-
for i
in range(
1, len(A)):
#第一个默认有序
-
tmp = A[i]
-
for j
in range(i
-1,
-1,
-1):
#j=i-1,j>=0,j--
-
if tmp < A[j]:
-
A[j+
1] = A[j]
-
A[j] = tmp
-
return A
4.希尔排序
希尔排序算法能够归纳为:先将整个待排序序列分割成若干个子序列(通常分红2个),分别进行直接插入排序,而后依次缩减增量再进行排序,待整个序列中整个元素增量为1时,再对全体元素进行一次直接插入排序。post
希尔排序是插入排序的一种高效率的实现,也叫缩小增量排序。简单的插入排序中,若是待排序列是正序时,时间复杂度是O(n),若是序列是基本有序的,使用直接插入排序效率就很是高。希尔排序就利用了这个特色。基本思想是:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。
希尔排序的分析是复杂的,时间复杂度是所取增量的函数,这涉及一些数学上的难题。可是在大量实验的基础上推出当n在某个范围内时,时间复杂度能够达到O(n^1.3)。
-
def ShellSort(A):
#希尔排序/至关加了个间隔,将数据分组处理
-
gap = len(A) /
2
-
while gap >=
1:
#下面就是一个插入排序过程,只是每一个过程都是有间隔,j+gap
-
for i
in range(gap, len(A)):
-
tmp = A[i]
-
j = i - gap
-
while j >=
0
and tmp < A[j]:
-
A[j+gap] = A[j]
-
j -= gap
-
A[j+gap] = tmp
-
gap /=
2
-
return A
5.快速排序
快速排序通常是选定第一个数为基准数,而后分别从后向前找比基准数小的数,从前向后找比基准数大的数,而后交换先后找到的数的位置,并在最后为基准数找到一个合适的位置,使得基准数左侧的数据都比基准数小,基准数右侧的数据都比基准数大,而后以基准数为界将序列分为左右2个子序列,最后利用递归分解的方法完成排序过程。
提示:在遇到选择或者填空题时,在作某一趟的快速排序推算时,用“挖坑填数法”+“分治法”,而在写程序时,用“交换法”+“分治法”。
快速排序在实际应用当中快速排序确实也是表现最好的排序算法。快速排序虽然高端,但其实其思想是来自冒泡排序,冒泡排序是经过相邻元素的比较和交换把最小的冒泡到最顶端,而快速排序是比较和交换小数和大数,这样一来不只把小数冒泡到上面同时也把大数沉到下面。
举个例子:对5,3,8,6,4这个无序序列进行快速排序,思路是右指针找比基准数小的,左指针找比基准数大的,交换之。
5,3,8,6,4 用5做为比较的基准,最终会把5小的移动到5的左边,比5大的移动到5的右边。
5,3,8,6,4 首先设置i,j两个指针分别指向两端,j指针先扫描(首先这也不是绝对的,这取决于基准数的位置,由于在最后两个指针相遇的时候,要交换基准数到相遇的位置。通常选取第一个数做为基准数,那么就是在左边,因此最后相遇的数要和基准数交换,那么相遇的数必定要比基准数小。因此j指针先移动才能先找到比基准数小的数。)4比5小中止。而后i扫描,8比5大中止。交换i,j位置。
5,3,4,6,8 而后j指针再扫描,这时j扫描4时两指针相遇。中止。而后交换4和基准数。
4,3,5,6,8 一次划分后达到了左边比5小,右边比5大的目的。以后对左右子序列递归排序,最终获得有序序列。
快速排序是不稳定的,其时间平均时间复杂度是O(nlgn)。
-
def QuickSort(A, left, right):
-
#left = 0
-
#right = len(A)-1
-
i = left
-
j = right
-
if i > j:
-
return
-
mid = A[i]
#初始值为第一个
-
while i < j:
-
#先从right高位开始
-
while i < j
and A[j] >=
mid:
-
j -=
1
-
A[i] = A[j]
#小的移到左边
-
while i < j
and A[i] <=
mid:
-
i +=
1
-
A[j] = A[i]
#大的移到右边
-
#print(i,j)
-
A[i] =
mid
#中间,也能够A[j]=mid,此时i=j
-
QuickSort(A, left, j
-1)
#左递归
-
QuickSort(A, i+
1, right)
#右递归
-
return A
6.堆排序
堆排序其实是利用堆的性质来进行排序的。
堆的定义:
堆其实是一棵彻底二叉树。
堆知足两个性质:
一、堆的每个父节点都大于(或小于)其子节点;
二、堆的每一个左子树和右子树也是一个堆。
堆的分类:
堆分为两类:
一、最大堆(大顶堆):堆的每一个父节点都大于其孩子节点;
二、最小堆(小顶堆):堆的每一个父节点都小于其孩子节点;
堆的存储:
通常都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。以下图所示:
堆排序:
由上面的介绍咱们能够看出堆的第一个元素要么是最大值(大顶堆),要么是最小值(小顶堆),这样在排序的时候(假设共n个节点),直接将第一个元素和最后一个元素进行交换,而后从第一个元素开始进行向下调整至第n-1个元素。因此,若是须要升序,就建一个大堆,须要降序,就建一个小堆。
堆排序的步骤分为三步:
一、建堆(升序建大堆,降序建小堆);
二、交换数据;
三、向下调整。
假设咱们如今要对数组arr[]={8,5,0,3,7,1,2}进行排序(降序):
首先要先建小堆:
堆建好了下来就要开始排序了:
-
class Solution(object):#小顶堆,降序
-
def HeapAdjust(self, A, i, n)
:
#删除
-
tmp = A[i]
#i表示当前节点开始调整,主要是通用性
-
index =
2*i+
1
-
while index <
n:
-
if index+
1 < n
and A[index+
1] < A[index]
:
#找到左右儿子最小值的索引
-
index +=
1
-
if tmp < A[index]
:
#知足该条件时说明原始堆有序
-
break
-
#将最小儿子上移动
-
A[i] = A[index]
-
i = index
-
index =
2*i+
1
-
A[i] = tmp
#temp一直没有变,并且用来做为比较的参考值
-
print(A)
-
def ConstructMinHeap(self, A, n)
:
#构建小根堆
-
#叶子节点不用参与重组,至关因而已经建好的堆
-
for i
in range(n/
2-
1, -
1, -
1):
-
self.HeapAdjust(A, i, n)
-
def HeapSort(self, A):
-
#第一次将A[0]与A[n - 1]交换,再对A[0…n-2]从新恢复堆,
-
#第二次将A[0]与A[n – 2]交换,再对A[0…n - 3]从新恢复堆,
-
#重复这样的操做直到A[0]与A[1]交换。
-
self.ConstructMinHeap(A, len(A))
#构建
-
for i
in range(len(A)-
1,
0, -
1):
-
tmp = A[i]
-
A[i] = A[
0]
-
A[
0] = tmp
-
self.HeapAdjust(A,
0, i)
-
return A
从一个无序序列建堆的过程就是一个反复筛选的过程。若将此序列当作是一个彻底二叉树,则最后一个非终端节点是n/2取底个元素,由此筛选便可。举个栗子:
对一个无序的序列A={5,4,17,13,15,12,10 }按从小到大进行排序,序列的下标分别为{1,2,3,4,5,6,7},A[i]表示下标为i的元素。
第一步:对无序的数组构造大根堆
大根堆的根节点是整个序列的最大值。
第二步:
将A[1]与A[7]互换,此时A[7]为序列的最大值,A[7]已经排序完毕,剩余的元素A[1]~A[6]造成新的未排序序列,因为此时序列不是大根堆,须要重构大根堆。
第三步:
将A[1]与A[6]互换,此时A[6]为序列的最大值,A[6]已经排序完毕,剩余的元素A[1]~A[5]造成新的未排序序列,因为此时序列不是大根堆,须要重构大根堆。
第四步:
将A[1]与A[5]互换,此时A[5]为序列的最大值,A[5]已经排序完毕,剩余的元素A[1]~A[4]造成新的未排序序列,因为此时序列不是大根堆,须要重构大根堆。
第五步:
将A[1]与A[4]互换,此时A[4]为序列的最大值,A[4]已经排序完毕,剩余的元素A[1]~A[3]造成新的未排序序列,因为此时序列不是大根堆,须要重构大根堆。
第六步:
将A[1]与A[3]互换,此时A[3]为序列的最大值,A[3]已经排序完毕,因为此时未排序的序列只剩下两个元素,并且A[0]>A[1],将A[0]与A[1]互换便可获得最终的已排序序列。
-
class Solution2(object):#大顶堆,升序
-
def HeapAdjust(self, A, i, n):
-
tmp = A[i]
-
index =
2*i+
1
#左右孩子的节点分别为2*i+1,2*i+2
-
while index <=
n:
-
#选择出左右孩子较小的下标
-
if index < n
and A[index] < A[index+
1]:
-
index +=
1
-
if tmp >= A[index]
:
#已经为大顶堆,=保持稳定性
-
break
-
A[i] = A[index]
#将子节点上移
-
i = index
#下一轮筛选
-
index *=
2
#右孩子的节点
-
A[i] = tmp
#temp一直没有变,插入正确的位置
-
print(A)
-
def ConstructMaxHeap(self, A, n)
:
#构建大根堆
-
#叶子节点不用参与重组,至关因而已经建好的堆
-
for i
in range(n/
2-
1, -
1, -
1):
-
self.HeapAdjust(A, i, n-
1)
-
def HeapSort2(self, A):
-
#第一次将A[0]与A[n - 1]交换,再对A[0…n-2]从新恢复堆,
-
#第二次将A[0]与A[n – 2]交换,再对A[0…n - 3]从新恢复堆,
-
#重复这样的操做直到A[0]与A[1]交换。
-
self.ConstructMaxHeap(A, len(A))
#构建
-
for i
in range(len(A)-
1, -
1, -
1):
-
tmp = A[i]
-
A[i] = A[
0]
-
A[
0] = tmp
-
self.HeapAdjust(A,
0, i-
1)
-
return A
7.归并排序
对于归并排序,记好一句话便可:递归的分解+合并。另外归并排序须要O(n)的辅助空间
归并排序是另外一种不一样的排序方法,由于归并排序使用了递归分治的思想,因此理解起来比较容易。其基本思想是,先递归划分子问题,而后合并结果。把待排序列当作由两个有序的子序列,而后合并两个子序列,而后把子序列当作由两个有序序列。。。。。倒着来看,其实就是先两两合并,而后四四合并。。。最终造成有序序列。空间复杂度为O(n),时间复杂度为O(nlogn)。
-
#/usr/bin/python
-
#coding:-*-utf
-8-*-
-
class Solution(object):
-
def MergeSort(self, A):
-
left =
0
-
right =
len(A)
-1
-
self.MergeArray(A,
left,
right)
-
return A
-
def Merge(self, A,
left,
mid,
right):
-
tmp = [
0]*(
right-
left+
1)#
len(A),初始化tmp中间数组
-
i =
left
-
j =
mid+
1
-
k =
0
-
while i <=
mid
and j <=
right:
-
if A[i] <= A[j]:
-
tmp[k] = A[i]
-
k +=
1
-
i +=
1
-
else:
-
tmp[k] = A[j]
-
k +=
1
-
j +=
1
-
print(A)
-
while i <=
mid:
-
tmp[k] = A[i]
-
k +=
1
-
i +=
1 #tmp[k++] = A[i++],不会发生越界,由于i也是先赋值,再++;
-
while j <=
right:
-
tmp[k] = A[j]
-
k +=
1
-
j +=
1
-
#将辅助空间内的数据转移到原始数组A
-
for p
in range(
len(tmp)):
-
A[
left+p] = tmp[p]
-
def MergeArray(self, A,
left,
right):
-
if
left >=
right:
-
return
-
mid = (
left+
right)/
2
-
self.MergeArray(A,
left,
mid)#左边
-
self.MergeArray(A,
mid+
1,
right)#右边
-
self.Merge(A,
left,
mid,
right)#合并
几种排序算法的性能比较
1:复杂度
平均复杂度:
O(N^2)的有冒泡排序、插入排序、选择排序
O(N*logN)的有希尔排序、归并排序、快速排序、堆排序
复杂度最坏状况:冒泡排序、插入排序、选择排序、快速排序均为O(N^2)(对于快速排序:最坏的状况,待排序的序列为正序或者逆序,每次划分只获得一个比上一次划分少一个的子序列,另一个为空。若是递归树画出来,就是一颗斜树。此时须要执行n-1次递归调用,且第i次划分须要经(n-i)次关键字比较才能找到才能找到第i个记录,所以比较的次数为(n-1)+(n-2)+…+1 = n*(n-1)/2,最终时间复杂度为O(n^2)),归并排序,堆排序均为O(N*logN)。
复杂度最好状况:冒泡排序、插入排序均为O(N),选择排序仍为O(N^2),归并排序,快速排序,堆排序仍为O(N*logN)。
最好、最坏、平均三项复杂度全是同样的、就是与初始排序无关的排序方法为:选择排序、堆排序、归并排序。
2:空间复杂度
除归并排序空间复杂度为O(N),快速排序空间复杂度为O(logN)外,其余几种排序方法空间复杂度均为O(1)
3:稳定性
所谓排序过程当中的稳定性是指:假定在待排序的记录序列中,存在多个具备相同的关键字的记录,若通过排序,这些记录的相对次序保持不变,则称这种排序算法是稳定的;不然称为不稳定的。
为稳定排序的有:冒泡排序,插入排序,归并排序;其他几种均为非稳定排序。
补充:找出若干个数中最大/最小的前K个数(K远小于n),用什么排序方法最好? 答:用堆排序是最好的。建堆O(n),k个数据排序klogn,总的复杂度为n+klogn。不考虑桶排序,n+klogn小于n*logn只有在k趋近n时才不成立,因此堆排序在绝大多数状况下是最好的。N较大时使用堆排序。