在笔试面试的过程中,常常会考察一下常见的几种排序算法,包括冒泡排序,选择排序,插入排序,希尔排序,快速排序,堆排序归并排序等7种排序算法,下面将分别进行讲解:
1.冒泡排序
所谓冒泡排序法,就是对一组数字进行从大到小或者从小到大排序的一种算法。具体方法是,相邻数值两两交换。从第一个数值开始,如果相邻两个数的排列顺序与我们的期望不同,则将两个数的位置进行交换(对调);如果其与我们的期望一致,则不用交换。重复这样的过程,一直到最后没有数值需要交换,则排序完成。一般地,如果有n个数需要排序,则需要进行(n-1)趟起泡。
冒泡排序是最简单的排序之一了,其大体思想就是通过与相邻元素的比较和交换来把小的数交换到最前面。这个过程类似于水泡向上升一样,因此而得名。举个栗子,对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)。
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]交换。
选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。举个栗子,对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数据,以形成一个更大的有序序列。
插入排序不是通过交换位置而是通过比较找到合适的位置插入元素来达到排序的目的的。相信大家都有过打扑克牌的经历,特别是牌数较大的。在分牌时可能要整理自己的牌,牌多的时候怎么整理呢?就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是一样的。举个栗子,对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时,再对全体元素进行一次直接插入排序。
希尔排序是插入排序的一种高效率的实现,也叫缩小增量排序。简单的插入排序中,如果待排序列是正序时,时间复杂度是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.堆排序
堆排序实际上是利用堆的性质来进行排序的。
堆的定义:
堆实际上是一棵完全二叉树。
堆满足两个性质:
1、堆的每一个父节点都大于(或小于)其子节点;
2、堆的每个左子树和右子树也是一个堆。
堆的分类:
堆分为两类:
1、最大堆(大顶堆):堆的每个父节点都大于其孩子节点;
2、最小堆(小顶堆):堆的每个父节点都小于其孩子节点;
堆的存储:
一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i 1和2 * i 2。如下图所示:
堆排序:
由上面的介绍我们可以看出堆的第一个元素要么是最大值(大顶堆),要么是最小值(小顶堆),这样在排序的时候(假设共n个节点),直接将第一个元素和最后一个元素进行交换,然后从第一个元素开始进行向下调整至第n-1个元素。所以,如果需要升序,就建一个大堆,需要降序,就建一个小堆。
堆排序的步骤分为三步:
1、建堆(升序建大堆,降序建小堆);
2、交换数据;
3、向下调整。
假设我们现在要对数组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较大时使用堆排序。