常用排序算法

来源:互联网 发布:销售分析软件 编辑:程序博客网 时间:2024/06/06 00:17

排序是程序设计中的一种重要操作,它的功能是将一个数据元素(或记录)的任意序列,重新排列成一个关键字有序的序列。

一、基本概念

1.稳定性

对序列中存在的任意的两个相等的数据元素A和B,如果A在B之前,在用某个排序算法排序后,A仍然在B之前,则就称该排序算法是稳定的;否则,就称该排序算法是不稳定的。
不稳定排序算法可能会更改相等数据元素的相对次序,但是稳定排序算法从来不会更改相等数据元素的相对次序。不稳定排序算法可以被特别地实现为稳定。
数据元素是否相等是和数据元素相关的,如果是语言支持的原子类型就是数值大小,如果是复杂的数据结构就按照该结构定义的比较方法进行比较。
衡量排序算法的好坏通常从以下方面考虑:
  1. 时间复杂度
  2. 空间复杂度
  3. 稳定性

二、常用算法

常用的排序算法包括:冒泡排序,选择排序,插入排序,希尔排序,快速排序,堆排序,归并排序

1.冒泡排序

1).基本概念

冒泡排序使用的基本方法是依次比较相邻的两个数,将小数放在前面,大数放在后面(或者大数放前边,小数放后边,这样实现的是元素递减排序)。
按照这样的方法,冒泡排序在第一趟依次比较第1个和第2个元素,第2个和第3个...,第n-1个和第n个,执行完后最大值(最小值)就到了第n个位置。
然后继续下一趟,这一趟比较的元素数目比上一趟少一个,因为在上一趟中由有一个元素找到了自己在排序好的序列中的位置。重复执行这一步直到整个序列都被排序。
冒泡排序的本质思想在于:遍历要排序的序列,并依次比较相邻的两个元素,如果它们的相对次序违反了排序的要求,就互换其位置;该算法将一直重复这个过程直到它的某一次遍历过程中没有发现任何需要交换位置的元素。

2).性能

  1. 时间复杂度:如需要排序的序列是已经排好序的,则该算法只需要遍历一遍序列,在遍历过程中只需进行n-1次比较,且不移动任何元素,此时其时间复杂度最好为O(n);如果要排序的序列的初始状态为"逆序",则需进行n(n-1)/2次比较和移动。此时其时间复杂度最差,为O(n^2)。
  2. 空间复杂度: 冒泡排序排序过程中只需要一个元素的空间用来完成元素交换
  3.  稳定性:冒泡排序是稳定的

2.选择排序

1) 基本概念

选择排序的思想很简单:每一趟从待排序的序列中选出最小(或最大)的一个元素,顺序放在已排好序的序列的合适位置(如果已排序序列放在整个序列的尾部,就是最前;如果已排序序列放在整个序列的头部,就是最后),直到全部待排序的数据元素排完。简单举例,要求对序列{9, 21, 13, 10,51, 70, 3, 4, 33, 50}进行递增排序
    1. 假设已排序序列放在整个序列头部,且某一趟排序完成后序列状态为{3, 4, 13, 10, 51, 70, 9, 21, 33, 50},则下一趟选出的最小值9应该放在已排序序列的最后即3,4的后边
    2. 假设已排序序列放在整个序列头部,且某一趟排序完成后序列状态为{9, 21, 13, 10, 33, 50, 3, 4, 51, 70},则下一趟选出的最大值50应该放在已排序序列的最前即51,70的前边
对比选择排序和冒泡排序,冒泡排序只要发现任何违反排序要求的元素序列,就交换它们的位置,而选择排序则比较懒惰,它发现一个比当前选择的元素大的元素时只记录它的位置,并在遍历过程中不断更新所选取元素的位置,只在本趟遍历结束时才进行一次元素的交换,因此在通常情况下选择排序的懒惰使得它具有比冒泡排序更好的性能。为什么只是通常呢,注意到冒泡排序可能存在“短路”的可能即提前退出的可能,它如果在一趟遍历过程中没有进行任何元素的位置交换就可以认为已经完成了排序,但是选择排序则无法提前退出,因而假如序列是已经排好序的冒牌排序的性能就优于选择排序。
从选择排序和冒泡排序的对比中可以得到一点启示:延迟费时的操作(比如这里的交换元素位置)有时候是可以提高性能的。

2) 性能

  1. 时间复杂度:选择排序的交换操作次数介于0 和 (n - 1) 次之间,最好情况是序列已经有序,交换0次;最坏情况是序列逆序,交换n-1次。选择排序的比较次数固定为 n (n - 1) / 2,比较次数与序列的初始状态无关。因此其时间复杂度为为O(n^2)。
  2. 空间复杂度:选择排序排序过程中只需要一个元素的空间用来完成元素交换
  3. 稳定性:选择排序是不稳定的

3.插入排序

1) 基本概念

插入排序的基本思想:
  1. 把要排序的序列分成有序部分和无序部分两部分,有序部分包含了序列中的第一个元素(或者最后一个元素)
  2. 依次将无序部分的每一个元素插入到有序部分中去
实际上只要进行排序,序列都可以看作由有序部分和无序部分两部分组成。但与冒泡排序和选择排序不同,冒泡排序和选择排序主要针对的是无序部分,其关键点在于从无序部分中找出其中的最大值(或最小值),然后再将其放入有序部分;而插入排序则主要针对的是有序部分,顺序从无序部分中取出每一个元素,然后将其插入到有序部分中去。就是这一点的改变就对性能有了很大的影响,因为有序部分是有规律的,因而插入时可以在找到合适位置时就退出,而不必一定遍历整个有序部分,这就节省了不少时间。
从比较中可以得到一点启示:针对有规律的数据进行设计很可能可以提高软件效率,降低复杂度。

2) 性能

  1. 时间复杂度: 如需要排序的序列是已经排好序的,则该算法只需要遍历一遍序列,在遍历过程中只需进行n-1次比较,且不移动任何元素,此时其时间复杂度最好为O(n)。如果要排序的序列的初始状态为"逆序",则需进行n(n-1)/2次比较和移动。此时其时间复杂度最差,为O(n^2)。
  2. 空间复杂度: 插入排序排序过程中只需要一个元素的空间用来完成元素交换
  3. 稳定性: 插入排序是稳定的
虽然时间复杂度都是O(n^2),但是插入排序的性能是好于冒泡排序和选择排序的。

4.希尔排序

1) 基本概念

希尔排序实际上是插入排序的一种高级版本。
其基本思想:
  1. 先取一个小于n的整数step1作为第一个步长,把文件的全部记录分成step1个组,其中所有距离为step1的倍数的记录放在同一个组中,各个组的起始元素分别为{1,2,3...step1}。
  2. 在各组上使用插入排序将每个组都排序
  3. 取第二个步长step2<step1重复上述的分组和排序,在这个过程中不断缩小步长直至所取的步长step=1即所有记录放在同一组中进行插入排序为止。
该方法实质上是一种分组插入方法。它充分利用了插入排序对有序的序列效率很高的特性,通过不断将序列分组排序使得序列逐步变得有序,最终在整个序列上使用插入排序。在刚开始排序时元素很无序,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长也变小了。
步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。好的步长序列的共同特征:
  • 最后一个步长必须为1
  • 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况
已知的最好步长序列由Marcin Ciura设计(1,4,10,23,57,132,301,701,1750,…),也有说是Sedgewick提出的 (1, 5, 19, 41, 109,...)

2) 性能

  1. 时间复杂度:希尔排序的时间复杂度取决于所使用的序列,序列最差时复杂度为O(n^2),序列最优时为 O(n (logn )^2)
  2. 空间复杂度:希尔排序排序过程中只需要一个元素的空间用来完成元素交换
  3. 稳定性:希尔排序是不稳定的

5.快速排序

1)基本概念

它的基本思想是将要排序的序列划分成两部分,其中一部分的所有元素都比另外一部分的任意元素都要小,然后再按此方法对这两部分分别进行快速排序,整个排序过程递归进行直到完成排序。
快速排序的关键点在于划分序列为两部分,它是分析快速排序性能的关键也是实现快速排序算法时的关键。一种快速排序的划分方法:
  1. 设置两个变量i、j,排序开始的时候:i=0,j=n-1;
  2. 以第一个数组元素作为关键数据,赋值给key,即 key=A[0];
  3. 从j开始向前搜索,即由后开始向前搜索(j--),直到j等于i或者找到一个小于key的值A[j],如果找到了一个小于key的A[j],就将其值赋给A[i];
  4. 从i开始向后搜索,即由前开始向后搜索(i++),直到i等于j或者找到一个大于key的值A[i],如果找到了一个大于key的A[i],就将其值赋给A[j];
  5. 如果i不等于j,就回到步骤2;
  6. 将key赋给A[i]
快速排序是目前已知的常用排序算法中最快的排序方法。

2)快速排序的性能

  1. 时间复杂度:快速排序的最坏情况发生在序列已经有序时,此时其时间复杂度为O(n^2)。为了防止这种情况发生可以在选择划分主元时采用其它的方法,比如采用随机选取的某个元素。快速排序的平均情况和最好情况的时间复杂度都为O(nlogn)。
  2. 空间复杂度:快速排序在排序过程中需要一个元素的空间来辅助完成元素交换
  3. 稳定性:快速排序是不稳定的

6.堆排序

1)基本概念

堆排序利用了堆这一数据结构。
堆:n个关键字序列Kl,K2,…,Kn称为堆当且仅当该序列满足如下性质:ki<=k(2i)且 ki<=k(2i+1)(1≤i≤ n)(这是小顶堆)或者ki>=k(2i)且 ki>=k(2i+1)(1≤i≤ n)(这是大顶堆),其中k(i)相当于二叉树的非叶结点,K(2i)则是左孩子,k(2i+1)是右孩子
若将此序列所存储的向量R[1..n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。
堆排序的方法:
  1. 将初始序列建成一个大顶堆(或小顶堆),此时得到了两个子序列,一个是不包含任何元素的有序子序列,一个是包含了所有元素的无序子序列,并且无序子序列是一个大顶堆(小顶堆)
  2. 将堆顶和无序序列的最后一个元素交换,由此得到新的少了一个元素的无序序列,同时一个新的元素被加入到了有序序列中
  3. 如果无序区的元素数目大于1,就调整无序区使其成为一个大顶堆(小顶堆),并回到步骤2;否则无序区只有一个元素,排序完成
可以看出堆排序实际上也是一种选择排序。与普通的选择排序相比,它实际上是通过堆这样的数据结构保存了排序过程中的中间结果,即如果两个元素已经做过比较,就将其相对大小通过堆来保存起来,这就可以减少比较次数,从而提高了性能。这种思想实际上是利用了计算机科学中的一个最基本的手段:缓存,利用缓存来缓存一些中间结果,在大多数情况下都可以提高软件的效率。

2)性能

  1. )时间复杂度:堆排序的时间主要用于调整堆的次序上,其最坏情况和平均情况的时间复杂度都为O(nlogn)
  2. 空间复杂度:堆排序在排序过程中需要一个元素的空间来辅助完成元素交换
  3. 稳定性:堆排序不稳定

7.归并排序

1)基本概念

归并排序的基本思想是将两个或两个以上的有序序列合并成一个新的有序序列。归并排序可以有两种方式来实现:
1.自顶向下
    1. 如果待排序序列只有一个元素,则排序完成退出;否则将该序列从中间划分称两个子序列
    2. 分别对两个子序列进行排序
    3. 合并两个排好序的子序列得到一个新的有序序列
2.自下向上
    1. 将序列看成有n个包含一个元素的子序列组成的序列,此时子序列长度为1
    2. 如果子序列长度大于等于n,则排序完成,退出
    3. 依次合并相邻的两个子序列,即子序列1和子序列2合并,子序列3和子序列4合并,一次类推
    4. 将子序列的长度乘以2,并回到步骤2

2)快速排序的性能

  1. 时间复杂度:归并排序的时间复杂度为O(nlogn)
  2. 空间复杂度:归并排序需要额外的n个辅助空间来辅助完成排序
  3. 稳定性:归并排序是稳定的排序算法