交换类排序主要是通过两两比较待排元素的关键字,若发现与排序要求相逆,则“交换”之。在这类排序方法中最常见的是起泡排序和快速排序,其中快速排序是一种在实际应用中具有很好表现的算法。
起泡排序(冒泡排序)
起泡排序的思想:首先,将 n 个元素中的第一个和第二个进行比较,如果两个元素的位置为逆序,则交换两个元素的位置;进而比较第二个和第三个元素关键字,如此类推,直到比较第 n-1 个元素和第 n 个元素为止;上述过程描述了起泡排序的第一趟排序过程,在第一趟排序过程中,我们将关键字最大的元素通过交换操作放到了具有 n 个元素的序列的最一个位置上。然后进行第二趟排序,在第二趟排序过程中对元素序列的前 n-1 个元素进行相同操作,其结果是将关键字次大的元素通过交换放到第 n-1 个位置上。一般来说,第 i 趟排序是对元素序列的前 n-i+1 个元素进行排序,使得前 n-i+1 个元素中关键字最大的元素被放置到第 n-i+1 个位置上。排序共进行 n-1 趟,即可使得元素序列按关键字有序。
//冒泡排序
public void main(String[] args){
int[] arr = new int[]{-12,3,2,34,5,8,1};
//冒泡排序
for(int i = 0;i < arr.length-1;i++){
for(int j = 0;j <arr.length-1-i;j++){
if(arr[j] >arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
空间效率:仅使用一个辅存单元O(1)。
时间效率:假设待排序的元素个数为 n,则总共要进行 n-1 趟排序,对 j 个元素的子序列进行一趟起泡排序需要进行 j-1 次关键字比较。由此,起泡排序的总比较次数为:
因此,起泡排序的时间复杂度为Ο(n^2)。
快速排序
快速排序是将分治法运用到排序问题中的一个典型例子,快速排序的基本思想是:通过一个枢轴(pivot)元素将 n 个元素的序列分为左、右两个子序列 Ll 和 Lr,其中子序列 Ll中的元素均比枢轴元素小,而子序列 Lr 中的元素均比枢轴元素大,然后对左、右子序列分别进行快速排序,在将左、右子序列排好序后,则整个序列有序,而对左右子序列的排序过程直到子序列中只包含一个元素时结束,此时左、右子序列由于只包含一个元素则自然有序。
用分治法的三个步骤来描述快速排序的过程如下:
- 划分步骤:通过枢轴元素 x 将序列一分为二, 且左子序列的元素均小于 x,右子序列的元素均大于 x;
- 治理步骤:递归的对左、右子序列排序;
- 组合步骤:无 从上面快速排序算法的描述中我们看到,快速排序算法的实现依赖于按照枢轴元素 x对待排序序列进行划分的过程。
对待排序序列进行划分的做法是:使用两个指针 low 和 high 分别指向待划分序列 r 的范围,取 low 所指元素为枢轴,即 pivot = r[low]。划分首先从 high 所指位置的元素起向前逐一搜索到第一个比 pivot 小的元素,并将其设置到 low 所指的位置;然后从 low 所指位置
的元素起向后逐一搜索到第一个比 pivot 大的元素,并将其设置到 high 所指的位置;不断重复上述两步直到 low = high 为止,最后将 pivot 设置到 low 与 high 共同指向的位置。使用上述划分方法即可将待排序序列按枢轴元素 pivot 分成两个子序列,当然 pivot 的选择不一定必须是 r[low],而可以是 r[low..high]之间的任何数据元素。
public void main(String[] args){
int [] a = {19,2,3,90,67,33,-7,24,3,56,34,5};
quickSort(a,0,a.length-1);
}
//快速排序
private void quickSort(int[] a, int low, int high) {
if(low<high){
int middle = getMiddle(a,low,high);
quickSort(a, 0, middle-1);
quickSort(a,middle+1,high);
}
}
//获取中间下标
private int getMiddle(int[] a, int low, int high) {
int temp = a[low];//基准元素
while(low<high){
while(low<high&&a[high]>=temp){
high--;
}
a[low] = a[high];
while(low<high&&a[low]<=temp){
low++;
}
a[high] = a[low];
}
a[low] = temp;//插入到排序后正确的位置
return low;
}
时间效率:快速排序算法的运行时间依赖于划分是否平衡,即根据枢轴元素 pivot 将序列划分为两个子序列中的元素个数,而划分是否平衡又依赖于所使用的枢轴元素。下面我们在不同的情况下来分析快速排序的渐进时间复杂度。
快速排序的最坏情况是每次进行划分时,在所得到的两个子序列中有一个子序列为空。此时,算法的时间复杂度T(n) = Tp(n) + T(n-1),其中Tp(n)是对具有n个元素的序列进行划分所需的时间,由以上划分算法的过程可以得到Tp(n) = Θ(n)。由此,T(n) =Θ(n) + T(n-1) =Θ(n2)。在快速排序过程中,如果总是选择r[low]作为枢轴元素,则在待排序序列本身已经有序或逆向有序时,快速排序的时间复杂度为Ο(n2),而在有序时插入排序的时间复杂度为Ο(n)。
快速排序的最好情况是在每次划分时,都将序列一分为二,正好在序列中间将序列分成长度相等的两个子序列。此时,算法的时间复杂度T(n) = Tp(n) + 2T(n/2),由于Tp(n) = Θ(n),所以T(n) = 2T(n/2) +Θ(n),由master method知道T(n) = Θ(n log n)。 在平均情况下,快速排序的时间复杂度 T(n) = kn ㏑ n,其中 k 为某个常数,经验证明,在所有同数量级的排序方法中,快速排序的常数因子 k 是最小的。因此就平均时间而言,快速排序被认为是目前最好的一种内部排序方法。
快速排序的平均性能最好,但是,若待排序序列初始时已按关键字有序或基本有序,则快速排序蜕化为起泡排序,其时间复杂度为Ο(n2)。为改进之,可以采取随机选择枢轴元素pivot的方法,具体做法是,在待划分的序列中随机选择一个元素然后与r[low]交换,再将r[low]作为枢轴元素,作如此改进之后将极大改进快速排序在序列有序或基本有序时的性能,在待排序元素个数n较大时,其运行过程中出现最坏情况的可能性可以认为不存在。
空间效率:虽然从时间上看快速排序的效率优于前述算法,然而从空间上看,在前面讨论的算法中都只需要一个辅助空间,而快速排序需要一个堆栈来实现递归。若每次划分都将序列均匀分割为长度相近的两个子序列,则堆栈的最大深度为 log n,但是,在最坏的情况
下,堆栈的最大深度为 n。