各类排序算法及其优化
排序总览
排序是是将一组“无序”的记录序列调整为“有序”的记录序列的过程。若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序;反之,若参加排序的记录数量很大,整个序列的排序过程不可能只在内存中完成,则称此类排序问题为外部排序。
排序的稳定性:若经过排序后,能使关键字相同的元素保持原记录序列中相对位置不变,则称排序算法是稳定的。
(注:稳定性并不能用来衡量一个排序算法的优劣,它只是对算法性质的一种描述。)
如非特别说明,本篇讨论的排序算法都是内部排序算法,且排序后的有序序列为从小到大的序列。
内部排序的过程是一个逐步扩大记录的有序序列长度的过程。
基于不同的“扩大” 有序序列长度的方法,内部排序方法大致可分下列几种类型:
插入类、交换类、选择类、归并类
一、插入类排序
将无序子序列中的一个或几个记录“插入”到有序序列中,从而增加记录的有序子序列的长度。
直接插入排序
一趟直接插入排序的基本思想:
直接插入排序实现一趟插入排序基本有三个步骤:
- 1、在R[1..i-1]中查找R[i]的插入位置,R[1..j]<R[i< R[j+1..i-1];
- 2、将R[j+1..i-1]中的所有记录均后移一个位置;
- 3、将R[i] 插入(复制)到R[j+1]的位置上。
代码实现:
public class Sort {
public static void insertionSort(int a[], int n){
int i, j;
for(i = 1; i < n; i ++){
int e = a[i];
for(j = i - 1; j >= 0 && e < a[j]; j --){
a[j + 1] = a[j];
}
a[j + 1] = e;
}
}
}
对于直接插入排序,最好的情况是已经有序,此时只需要进行次比较,而不需要进行移动;而最坏的情况是记录完全逆序,此时进行比较和移动的次数都是级别的。故直接插入排序的时间复杂度为,且由其过程可以看到,直接插入排序算法是稳定的。
折半插入排序
观察直接插入排序的过程我们发现,我们是把一个个未排序元素的往有序序列里插入,这样查找插入位置可以使用折半查找,最后再统一向后移动元素,于是有了折半插入排序:
public class Sort{
public static void semiInsertionSort(int a[], int n){
int i, j, low, high, mid, e;
for(i = 1; i < n; i ++){
e = a[i]; low = 0; high = i - 1;
while(low <= high){
mid = (low + high) / 2;
if(e > a[mid])
low = mid + 1;
if(e <= a[mid])
high = mid -1;
}
for(j = i - 1; j > high; j -- )
a[j + 1] = a[j];
a[high + 1] = e;
}
}
折半插入排序一定程度上减少了元素比较的次数,但元素移动次数并没改变,因此折半插入排序的时间复杂度仍然是,它同样是一种稳定的排序算法。
希尔排序
希尔排序的基本思想是,对待排记录序列先作“宏观”调整,再作“微观”调整。将记录序列分成若干子序列,分别对每个子序列进行插入排序。如将n个记录分为d个子序列:
希尔排序其中,d 称为增量,它的值在排序过程中从大到小逐渐缩小,直至最后一趟排序减为 1。这里实现的希尔排序为了逻辑清晰,使用的是每次除以2的增量序列,实际这个序列可以当做参数传入,更好的增量序列可以有更好的效果。
public class Sort{
public static void shellSort(int a[], int n){
int i, j;
for(int d = n/2; d >= 1; d = d/2)
for(i = d; i < n; i ++){
int e = a[i];
for(j = i - d; j >= 0 && a[j] > e; j -= d){
a[j + d] = a[j];
}
a[j + d] = e;
}
}
}
希尔排序的时间复杂度分析比较复杂,但是可以严格证明,只要增量序列选取合适,希尔排序的平均时间复杂度是可以小于的,但仍然大于。由于相同关键字可能被划分到不同的子序列,故希尔排序是不稳定的。
二、交换类排序
交换排序通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。
冒泡排序
冒泡排序的基本思想是,假设待排序序列长度为n,从后往前依次两两比较相邻元素,若逆序则交换它们,这样的一轮比较称为一次冒泡,每轮冒泡其结果就是最小的元素浮现在第一位(水面),这也是冒泡排序名称的由来。
public class Sort{
public static void bubbleSort(int a[], int n){
boolean flag;
for(int i = 0; i < n; i ++) {
flag = false;
for(int j = n - 1; j >i; j --){
if(a[j] < a[j - 1]) {
swap(a, j, j-1);
flag = true;
}
}
if(flag == false) return;
}
}
public static void swap(int[] arr, int i, int j) {
//辅助函数,用于交换
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
显然冒泡排序的时间复杂度是,由于相邻比较相等时并不会交换,故冒泡排序是一种稳定的排序。
快速排序
快速排序基于分治,其基本思想是找一个记录,以它的关键字作为“枢轴”,凡其关键字小于枢轴的记录均移动至该记录之前,反之,凡关键字大于枢轴的记录均移动至该记录之后。致使一趟排序(划分)之后,记录的无序序列R[s..t]将分割成两部分:R[s..i-1]和R[i+1..t],且R[j]≤ R[i]≤ R[j] (s≤j≤i-1)枢轴(i+1≤j≤t),之后分别对分割所得两个子序列递归进行划分。
一趟快速排序'''一般快速排序'''
public class Sort{
public static void quickSort(int a[], int low, int high){
int i = low, j = high, temp;
if(i < j){
temp = a[low];//划分枢轴
while(i < j) {//避免顺序情况出现下越界
while (i < j && a[j] >= temp) j--;
a[i] = a[j];
while (i < j && a[i] <= temp) i++;
a[j] = a[i];
}
a[i] = temp;
quickSort(a, low, i - 1);
quickSort(a, i + 1, high);
}
}
}
平均而言快速排序是一个的算法,不过实际快速排序的时间复杂度与划分是否对称有关,因此枢轴的选取很重要,在上面给出的快速排序代码中,每次使用的枢轴都是划分后各序列的最左边的元素,这就有一个问题,(最坏情况)如果原始序列是本就有序的或逆序的,这样快速排序就退化为一个的算法,并体现不出其快。为了解决这个问题,我们每次选取枢轴时可以随机从划分后的序列中选取,而不是只选择最左边那个:
'''应对几乎有序或逆序情况的改进快速排序'''
public class Sort{
public static void quickSort(int a[], int low, int high){
swap( a, low , (int)(Math.random()*(high-low+1)) + low );
int i = low, j = high, temp;
if(i < j){
temp = a[low];//划分枢轴
while(i < j) {//避免顺序情况出现下越界
while (i < j && a[j] >= temp) j--;
a[i] = a[j];
while (i < j && a[i] <= temp) i++;
a[j] = a[i];
}
a[i] = temp;
quickSort(a, low, i - 1);
quickSort(a, i + 1, high);
}
}
}
可以看到代码只是加入了一行实现随机选取序列中的元素与第一个元素进行交换,此时虽然仍有可能退化为的算法,但是可以严格证明,当n趋于无穷时,这种情况发生的概率为0,它是一个绝对平均的算法。
还能不能继续优化呢?其实对于所有的高级排序算法都有一个可用的优化,那就是当规模比较小时转而使用插入排序,这样往往会进一步提高排序效率,这是因为当规模小到一定程度时小规模数据基本有序的概率大大增加,而插入排序对基本有序序列时间复杂度是接近的,所以给出快速排序的第三个优化:
'''小规模转用插入排序'''
public class Sort{
public static void quickSort3(int a[], int low, int high){
if(high - low <= 12){
insertionSortH(a, low, high);
return ;
}
swap( a, low , (int)(Math.random()*(high-low+1)) + low );
int i = low, j = high, temp;
if(i < j){
temp = a[low];//划分枢轴
while(i < j) {//避免顺序情况出现下越界
while (i < j && a[j] >= temp) j--;
a[i] = a[j];
while (i < j && a[i] <= temp) i++;
a[j] = a[i];
}
a[i] = temp;
quickSort(a, low, i - 1);
quickSort(a, i + 1, high);
}
}
public static void insertionSortH(int arr[], int l, int r){
//插入排序l...r
for( int i = l+1 ; i <= r ; i ++ ) {
int e = arr[i];
int j;
for (j = i; j > l && arr[j-1] > e; j--)
arr[j] = arr[j-1];
arr[j] = e;
}
return;
}
}
综合来讲快速排序算法的时间复杂度平均为。由于相同元素可能被划分开与其它元素交换后相对位置改变,故快速排序是一种不稳定的排序。
三、选择类排序
选择类排序基本思想是从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。
简单选择排序
简单选择排序比较简单,
选择排序直接给出代码:
public class Sort{
public static void selectionSort(int a[], int n){
for(int i = 0; i< n -1; i ++){
int minIndex = i;
for(int j = i + 1; j < n; j ++)
if(a[minIndex] > a[j])
minIndex = j;
swap(a, i, minIndex);
}
}
}
简单选择排序元素的比较次数和序列初始状态无关,故时间复杂度始终为,且交换改变相对位置,故是一种不稳定的排序。
堆排序
在算法之美系列中,数据结构之优先队列这篇文章详细介绍了堆这种数据结构数据结构——堆,这里不再赘述建堆和堆的维护过程,其排序的基本思想是建立一个最小堆,每次取出堆顶元素,并维护堆,这样得到的序列就是有序的。堆排序的代码:
public class Sort{
public static void siftDown(int a[], int k, int n){
while(2*k + 1 < n){//大于则是左孩子越界
int j = 2*k + 1;
if(j + 1 < n && a[j + 1] > a[j]){
//右孩子大于左孩子
j ++;//取右孩子
}
if(a[k] >= a[j]){
break;
}
swap(a, k, j);
k = j;
}
}
public static void maxHeap(int a[], int n){
//建堆过程
for(int i = (n-1-1)/2; i >= 0; i --)
siftDown(a, i, n);
}
public static void heapSort(int a[], int n){
//堆排序
maxHeap(a, n);
for(int i = n; i>0; i --){
swap(a, 0, i - 1);
siftDown(a, 0, i-1);
}
}
}
堆排序建堆的时间复杂度为,维护调整堆的时间复杂度为(因为堆是完全二叉树结构,其高度为logn级别),故堆排序时间复杂度为,且显然是一种不稳定的排序。
四、归并类排序
归并排序的基本思想是通过合并两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度,使之有序。(内部排序通常都是二路归并。)
递归式归并排序
public class Sort{
public static void merge(int a[], int l, int m, int r){
//归并过程
int b[] = new int[r - l + 1];
for(int i =l; i <= r; i++)
b[i-l] = a[i];
int i = l, j = m+1,k;
for(k = i; i <= m && j <= r; k++){
if(b[i-l] < b[j-l]){
a[k] = b[i ++ -l]; // a[k] = b[i -l]; i ++;
}
else{
a[k] = b[j -l];
j ++;
}
}
while(i<=m) a[k++] = b[i++ -l];
while(j<=r) a[k++] = b[j++ -l];
}
public static void mergeSort(int a[], int low, int high){
//归并排序
if(low<high){
int mid = (low+high) / 2;
mergeSort(a, low, mid);
mergeSort(a, mid+1, high);
if(a[mid] > a[mid + 1]) //这里是优化,去掉也可以执行,加
//上的意思是若现在已经有序了,就不用比较了
merge(a,low,mid, high);
}
}
}
迭代式归并排序(非递归)
public class Sort{
public static void mergeSortBU(int a[], int n){
for(int sz = 1; sz <= n; sz += sz)
for(int i = 0; i + sz < n; i += 2*sz)
merge(a, i, i + sz - 1,min(i + 2 * sz - 1, n - 1));
}
}
对于归并排序,同样可以加入当规模比较小时,使用插入排序以提高性能(实际上每个高级排序算法都可以有这个优化。)
全部代码封装为排序类
将以上代码封装到一个类中,
排序类:
'''Sort.java'''
import static java.lang.Integer.min;
public class Sort {
public static void insertionSort(int a[], int n){
int i, j;
for(i = 1; i < n; i ++){
int e = a[i];
for(j = i - 1; j >= 0 && e < a[j]; j --){
a[j + 1] = a[j];
}
a[j + 1] = e;
}
}
public static void semiInsertionSort(int a[], int n){
int i, j, low, high, mid, e;
for(i = 1; i < n; i ++){
e = a[i]; low = 0; high = i - 1;
while(low <= high){
mid = (low + high) / 2;
if(e > a[mid])
low = mid + 1;
if(e <= a[mid])
high = mid -1;
}
for(j = i - 1; j > high; j -- )
a[j + 1] = a[j];
a[high + 1] = e;
}
}
public static void shellSort(int a[], int n){
int i, j;
for(int d = n/2; d >= 1; d = d/2)
for(i = d; i < n; i ++){
int e = a[i];
for(j = i - d; j >= 0 && a[j] > e; j -= d){
a[j + d] = a[j];
}
a[j + d] = e;
}
}
public static void bubbleSort(int a[], int n){
boolean flag;
for(int i = 0; i < n; i ++) {
flag = false;
for(int j = n - 1; j >i; j --){
if(a[j] < a[j - 1]) {
swap(a, j, j-1);
flag = true;
}
}
if(flag == false) return;
}
}
public static void quickSort(int a[], int low, int high){
int i = low, j = high, temp;
if(i < j){
temp = a[low];//划分枢轴
while(i < j) {//避免顺序情况出现下越界
while (i < j && a[j] >= temp) j--;
a[i] = a[j];
while (i < j && a[i] <= temp) i++;
a[j] = a[i];
}
a[i] = temp;
quickSort(a, low, i - 1);
quickSort(a, i + 1, high);
}
}
public static void quickSort2(int a[], int low, int high){
swap( a, low , (int)(Math.random()*(high-low+1)) + low );
int i = low, j = high, temp;
if(i < j){
temp = a[low];//划分枢轴
while(i < j) {//避免顺序情况出现下越界
while (i < j && a[j] >= temp) j--;
a[i] = a[j];
while (i < j && a[i] <= temp) i++;
a[j] = a[i];
}
a[i] = temp;
quickSort(a, low, i - 1);
quickSort(a, i + 1, high);
}
}
public static void quickSort3(int a[], int low, int high){
if(high - low <= 12){
insertionSortH(a, low, high);
return ;
}
swap( a, low , (int)(Math.random()*(high-low+1)) + low );
int i = low, j = high, temp;
if(i < j){
temp = a[low];//划分枢轴
while(i < j) {//避免顺序情况出现下越界
while (i < j && a[j] >= temp) j--;
a[i] = a[j];
while (i < j && a[i] <= temp) i++;
a[j] = a[i];
}
a[i] = temp;
quickSort(a, low, i - 1);
quickSort(a, i + 1, high);
}
}
public static void selectionSort(int a[], int n){
for(int i = 0; i< n -1; i ++){
int minIndex = i;
for(int j = i + 1; j < n; j ++)
if(a[minIndex] > a[j])
minIndex = j;
swap(a, i, minIndex);
}
}
public static void siftDown(int a[], int k, int n){
while(2*k + 1 < n){//大于则是左孩子越界
int j = 2*k + 1;
if(j + 1 < n && a[j + 1] > a[j]){
//右孩子大于左孩子
j ++;//取右孩子
}
if(a[k] >= a[j]){
break;
}
swap(a, k, j);
k = j;
}
}
public static void maxHeap(int a[], int n){
//建堆过程
for(int i = (n-1-1)/2; i >= 0; i --)
siftDown(a, i, n);
}
public static void heapSort(int a[], int n){
//堆排序
maxHeap(a, n);
for(int i = n; i>0; i --){
swap(a, 0, i - 1);
siftDown(a, 0, i-1);
}
}
public static void merge(int a[], int l, int m, int r){
//归并过程
int b[] = new int[r - l + 1];
for(int i =l; i <= r; i++)
b[i-l] = a[i];
int i = l, j = m+1,k;
for(k = i; i <= m && j <= r; k++){
if(b[i-l] < b[j-l]){
a[k] = b[i ++ -l]; // a[k] = b[i -l]; i ++;
}
else{
a[k] = b[j -l];
j ++;
}
}
while(i<=m) a[k++] = b[i++ -l];
while(j<=r) a[k++] = b[j++ -l];
}
public static void mergeSort(int a[], int low, int high){
//归并排序
if(low<high){
int mid = (low+high) / 2;
mergeSort(a, low, mid);
mergeSort(a, mid+1, high);
if(a[mid] > a[mid + 1]) //这里是优化,去掉也可以执行,加
//上的意思是若现在已经有序了,就不用比较了
merge(a,low,mid, high);
}
}
public static void mergeSortBU(int a[], int n){
for(int sz = 1; sz <= n; sz += sz)
for(int i = 0; i + sz < n; i += 2*sz)
merge(a, i, i + sz - 1,min(i + 2 * sz - 1, n - 1));
}
public static void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
public static void insertionSortH(int arr[], int l, int r){
for( int i = l+1 ; i <= r ; i ++ ) {
int e = arr[i];
int j;
for (j = i; j > l && arr[j-1] > e; j--)
arr[j] = arr[j-1];
arr[j] = e;
}
return;
}
public static void main(String args[]){
int a[] = {3,7,4,6,5,9,1,-9,67};
Sort.mergeSortBU(a, a.length - 1);
for(int i = 0; i<a.length; i++)
System.out.println(a[i]);
}
}