内部排序算法

本文记录了我在面试过程中感觉有用的问题,方便日后参考。



前言

一、排序的目的

为了查找方便,通常希望计算机中的查找表是按关键字有序的,因为此时可以使用查找效率较高的折半查找。

二、排序的概念

将一个数据元素的任意序列,重新排列成一个按关键字有序的序列。

三、内部排序和外部排序

根据排序时待排序的数据元素数量的不同,使得排序过程中涉及的存储器不同,可以将排序方法分为两类。
一类是整个排序过程在内存储器中进行,称为内部排序;另一类是由于待排序元素数量太大,
以至于内存储器无法容纳全部数据,排序需要借助外部存储设备才能完成,这类排序称为外部排序。
本文涉及的排序均为内部排序。

四、排序结果是否唯一

如果是按照主关键字来排序,则得到的排序结果是唯一的。否则,如果按照次关键字,则得到的结果是不唯一的。

五、排序是否稳定

如果在待排序的序列中存在多个具有相同关键字的元素。假设元素 Ri 在 Rj 之前,两者关键字
相同,如果排序过后 Ri 仍在 Rj 前面,则称排序方法是稳定的,否则,当相同关键字元素的
前后关系在排序中发生变化,则称排序方法是不稳定的。无论是稳定的还是不稳定的排序方法,
均能完成排序的功能。在某些场合可能对排序有稳定性的要求,此时就应当选择稳定的排序方法。

六、排序的分类

按照排序过程中依据的原则对内部排序进行分类,大致可以分为插入排序、交换排序、选择排序、
归并排序等。

插入类排序:

基本思想:逐个考察每个待排序元素,将每一个新元素插入到前面已经排好序的序列中适当的位置上,
使得新序列仍然是一个有序序列。

类别:直接插入排序、折半插入排序、希尔排序。

交换类排序主要是通过两两比较待排元素的关键字,若发现与排序要求相逆,则“交换”
之。在这类排序方法中最常见的是起泡排序和快速排序,其中快速排序是一种在实际应用中
具有很好表现的算法。

选择排序的基本思想是:每一趟从n-i+1 (i=1,2,…,n)个元素中选取一个关键字最小的元
素作为有序序列中第i 个元素。本节在介绍简单选择排序的基础上,给出了对其进行改进的
算法——树型选择排序和堆排序。

975748-20160920075116543-823705692.jpg

七、基于比较的排序的对比

插入排序、交换排序、选择排序、归并排序等排序方法,都有一个共同的特点,那就是它们都是通过
比较元素的大小来确定元素之间的相对位置的,都是基于比较的排序方法。

从算法的平均时间复杂度、最坏时间复杂度、空间复杂度以及排序的稳定性等方面,对各种排序方法加以比较。

排序方法平均时间复杂度最坏时间复杂度最好时间复杂度空间复杂度稳定性复杂性
直接插入排序O(\(n^2\))O(\(n^2\))O(n)O(1)稳定简单
希尔排序O(\(nlog_2n\))O(\(nlog_2n\))O(1)不稳定较复杂
冒泡排序O(\(n^2\))O(\(n^2\))O(n)O(1)稳定简单
快速排序O(\(nlog_2n\))O(\(n^2\))O(\(nlog_2n\))O(\(nlog_2n\))不稳定较复杂
直接选择排序O(\(n^2\))O(\(n^2\))O(\(n^2\))O(1)不稳定简单
堆排序O(\(nlog_2n\))O(\(nlog_2n\))O(\(nlog_2n\))O(1)不稳定较复杂
归并排序O(\(nlog_2n\))O(\(nlog_2n\))O(\(nlog_2n\))O(n)稳定较复杂
基数排序O(d(n+r))O(d(n+r))O(d(n+r))O(n+r)稳定较复杂

通过对排序可能出现的结果个数的研究,可以得出如下结论:
任何一个基于比较操作的排序方法,在最坏情况下所需要进行的比较次数至少为 nlogn 次,
即算法的时间复杂度下界为 Ω(nlogn)。




详细介绍

直接插入排序(简单插入排序)代码

基本思想:在要排序的一组数中,假设前面(n-1) [n>=2] 个数已经是排好顺序的,
现在要把第n个数插到前面的有序数中,使得这n个数也是排好顺序的。
如此反复循环,直到全部排好顺序。

public static void insertSort(int[] r, int low, int high){
    for(int tmp, j, i = low + 1; i <= high; i++){
        if(r[i] < r[i-1]){
            tmp = r[i];
            j = i - 1;
        
            for(; j >= low && tmp < r[j]; j--)
                r[j+1] = r[j];
            
            r[j+1] = tmp;
        }
    }
}
希尔排序(最小增量排序)

基本思想:算法先将要排序的一组数按某个增量d(n/2,n为要排序数的个数)分成若干组,
每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的
增量(d/2)对它进行分组,在每组中再进行直接插入排序。当增量减到1时,
进行直接插入排序后,排序完成。


//method one
public static void shellSort(int[] r, int low, int high){
    
    for(int gap = r.length/2; gap > 0; gap /= 2){
        for(int m = 0; m < gap; m++){
            for(int tmp, j, i = low + gap; i <= high; i += gap){
                if(r[i] < r[i-gap]){
                    tmp = r[i];
                    j = i - gap;
                    
                    for(; j >= low && tmp < r[j]; j -= gap)
                        r[j+gap] = r[j];
                        
                    r[j+gap] = tmp; 
                }
            }
        }           
    }       
}

//method two
public static void shellSortII(int[] r, int low, int high){
    
    for(int gap = r.length/2; gap > 0; gap /= 2){
        for(int tmp, j, i = gap; i <= high; i++){
            if(r[i] < r[i-gap]){
                tmp = r[i];
                j = i - gap;
                
                for(; j >= low && tmp < r[j]; j -= gap)
                    r[j+gap] = r[j];
                    
                r[j+gap] = tmp; 
            }
        }
    }
}

//method three
public static void shellSortIII(int[] r, int low, int high){
    for(int gap = r.length/2; gap > 0; gap /= 2){
        for(int i = gap; i <= high; i++){
            for(int j = i-gap; j >= 0 && r[j] > r[j+gap]; j -= gap)
                swap(r, j, j+gap);
        }
    }
}

private static void swap(int[] r, int i, int j){
    int tmp = r[i];
    r[i] = r[j];
    r[j] = tmp;
}   
冒泡排序代码

基本思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,
自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,
较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。


public static void bubbleSort(int[] r, int low, int high){
    
    for(int i = low; i <= high; i++){
        for(int j = i+1; j <= high; j++){
            if(r[i] > r[j]){
                swap(r, i, j);
            }
        }
    }
}

private static void swap(int[] r, int i, int j){
    int tmp = r[i];
    r[i] = r[j];
    r[j] = tmp;
}
快速排序代码

基本思想:选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,
将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,
此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。


public static void quickSort(int[] r, int low, int high) {
    if (low < high) {
        int pa = partition(r, low, high);
        quickSort(r, low, pa - 1);
        quickSort(r, pa + 1, high);
    }
}

private static int partition(int[] r, int low, int high) {
    int pivot = r[low]; // 使用r[low]作为枢轴元素
    while (low < high) { // 从两端交替向内扫描
        while (low < high && r[high] > pivot)
            high--;
        
        r[low] = r[high]; // 将比pivot 小的元素移向低端
        
        while (low < high && r[low] < pivot)
            low++;
        
        r[high] = r[low]; // 将比pivot 大的元素移向高端
    }
    r[low] = pivot; // 设置枢轴
    return low; // 返回枢轴元素位置
}
直接选择排序(简单选择排序)

基本思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换;
然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。

public static void selectSort(int r[], int low, int high){
            
    for(int i = low; i <= high; i++){
        int min = i;
        for(int j = i+1; j <= high; j++){
            if(r[j] < r[min]){
                min = j;
            }
        }
        
        swap(r, i, min);
    }
}

private static void swap(int[] r, int i, int j){
    int tmp = r[i];
    r[i] = r[j];
    r[j] = tmp;
}
树形选择排序(锦标赛排序)

基本思想: 先把待排序的 n 个元素两两进行比较,取出较小者,若轮空则直接进入下一轮比较;
然后在[n/2]个较小者中,采用同样的方法进行比较,再选出较小者;如此反复,直到选出关键字最小的元素为止。
这个过程可以使用一颗具有 n 个结点的完全二叉树来表示,最终选出的关键字最小的元素就是这棵二叉树的根结点。
在输出关键字最小的元素后,为选出次小关键字,可以将最小关键字元素所对应的叶子结点的关键字设置为∞,
然后从该叶子结点起逆行向上,将所经过的结点与其兄弟进行比较,修改从该叶子结点到根结点上各结点的值,则根结点的值即为次小关键字。


public static void treeSelectSort(int[] r, int low, int high){
    
    int len = high - low + 1;
    int treeSize = 2 * len - 1;
    int[] tree = new int[treeSize];
    
    //fill leaf nodes
    for(int i=len-1, j=0; i >= 0; i--, j++){
        tree[treeSize-1-j] = r[i];
    }
    
    //fill non-leaf nodes
    for(int i=treeSize-1; i > 0; i -= 2){
        tree[(i-1)/2] = tree[i-1] < tree[i] ? tree[i-1] : tree[i];
    }
    
    int minIndex;
    
    while(low <= high){
        int min = tree[0];
        r[low++] = min;
        minIndex = treeSize - 1;
        
        while(tree[minIndex] != min)
            minIndex--;
            
        tree[minIndex] = Integer.MAX_VALUE;
        
        while(minIndex > 0){ //if it has parent node
            if(minIndex % 2 == 0){ //if it's the right node
                tree[(minIndex-1)/2] = tree[minIndex-1] < tree[minIndex] ? tree[minIndex-1] : tree[minIndex];
                minIndex = (minIndex - 1)/2;
            }
            else { //if it's the left node
                tree[minIndex/2] = tree[minIndex] < tree[minIndex+1] ? tree[minIndex] : tree[minIndex+1];
                minIndex = minIndex/2;
            }
        }
    }
    
}
堆排序

基本思想: 设有n 个元素,欲将其按关键字排序。可以首先将这n 个元素按关键字建成堆,将堆顶
元素输出,得到n 个元素中关键字最大(或最小)的元素。然后,再将剩下的n-1 个元素重
新建成堆,再输出堆顶元素,得到n 个元素中关键字次大(或次小)的元素。如此反复执行,
直到最后只剩一个元素,则可以得到一个有序序列,这个排序过程称之为堆排序。


public static void heapSort(int[] r, int low, int high){
    
    for(int i=high/2; i >= 0; i--)
        heapAdjust(r, i, high);
    
    int tmp;
    for(int i=high; i >= 0; i--){
        tmp = r[0];
        r[0] = r[i];
        r[i] = tmp;
        heapAdjust(r, 0, i-1);
    }
}

private static void heapAdjust(int[] r, int low, int high){ 
    int tmp = r[low];
    for(int j=2*low+1; j <= high; j *= 2){
        if(j < high && r[j] < r[j+1])
            j++;
        if(tmp >= r[j])
            break;
        r[low] = r[j];
        low = j;
    }
    r[low] = tmp;
}
归并排序

基本思想: 归并排序是建立在归并操作上的一种有效的排序算法,是采用分治法(divide and conquer)的一个
典型应用。先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

public static void mergeSort(int[] r, int low, int high){
    if(low < high){
        int center = (low+high)/2;
        mergeSort(r, low, center);
        mergeSort(r, center+1, high);
        merge(r, low, center, high);
    }
}

private static void merge(int[] r, int left, int center, int right){
    int[] tmpArr = new int[right-left+1];
    int middle = center + 1;
    int start = 0;
    int index = left;
        
    while(left <= center && middle <= right){
        if(r[left] <= r[middle]){
            tmpArr[start++] = r[left++];
        }else {
            tmpArr[start++] = r[middle++];
        }
    }
    
    while(left <= center)
        tmpArr[start++] = r[left++];
        
    while(middle <= right)
        tmpArr[start++] = r[middle++];
        
    for(int i=0; i < tmpArr.length; i++)
        r[index+i] = tmpArr[i];
}


//归并排序自底向上的方式
/*
1 xx|xx|xx|xx|xx|xx|xx|xx| 
2 xxxx|xxxx|xxxx|xxxx| 
3 xxxxxxxx|xxxxxxxx 
*/
public static void bottomUpMergeSort(int[] r, int low, int high){
    int size = high - low + 1;
    
    //外循环为每次归并排序,每组数据的宽度,每组数据的宽度之后进行2倍递增
    for(int width=1; width < size; width *= 2){
    
        //内循环为基于每组数据的宽度,进行多组数据的归并排序
        //index += 2 * width 因为一次归并排序都是使用 2 组数据进行排序,
        //所以每次递增两组数据的偏移量
        //index < (size - width) 表示排序至少需要一组多的数据
        for(int index=0; index < (size-width); index += 2*width){
            int lo = index;
            int hi = index + (2 * width - 1);
            int mid = index + (hi - lo)/2;
            merge(r, lo, mid, hi);
        }
    }
}
计数排序

基本思想: 计数排序是一种类似桶排序的算法,时间复杂度 O(n),其优势是对已知数量范围
的数组进行排序。它创建一个长度为这个数据范围的数组,这个数组的每个元素记录排序数组
中对应记录出现的个数。


//方案一,来自麻省的教材
public static void countingSort(int[] in, int[] out, int k){
    int[] temp = new int[k+1];
    
    for(int i=0; i < in.length; i++)
        temp[in[i]]++;
    
    for(int i=1; i <= k; i++)
        temp[i] += temp[i-1];
        
    for(int i=in.length-1; i >= 0; i--){
        out[temp[in[i]]-1] = in[i];
        temp[in[i]]--;
    }
    
}

//方案二
public static void countingSortII(int[] r, int k){
    
    int[] temp = new int[k+1];
    
    for(int i=0; i < r.length; i++)
        temp[r[i]]++;
        
    for(int z=0, i=0; i <= k; i++){
        while(temp[i]-- > 0)
            r[z++] = i;
    }
    
}
桶排序(箱排序)

基本思想: 设待排序序列的元素取值范围为 0 到 m ,则我们新建一个大小为 m+1 的临时数组并把初始值都设为0,
遍历待排序序列,把待排序序列中元素的值作为临时数组的下标,找出临时数组中对应该下标的元素使之加1;
然后遍历临时数组,把临时数组中元素大于 0 的下标作为值按次序依次填入待排序数组,元素的值作为重复填入该下标的次数,
遍历完成则排序结束序列有序。


public static void bucketSort(int[] r, int max){
    int[] tmp = new int[max+1];
    
    for(int i=0; i < r.length; i++)
        tmp[r[i]]++;
        
    for(int i=0, j=0; i <= max; i++)
        for(int k=0; k < tmp[i]; k++)
            r[j++] = i;
}
基数排序

基本思想: 基数排序不需要比较关键字的大小,它是根据关键字中各位的值,通过对排序的 n 个元素进行
若干趟“分配”与“收集”来实现排序的。


//获取指定位上的数字
private static int getDigit(int x, int d){
    int value = 1;
    for(int i=1; i < d; i++)
        value *= 10;
    
    return (x/value) % 10;
}

public static void radixSort(int[] r, int low, int high, int digit){
    final int radix = 10; //基数
    int[] count = new int[radix]; //存放各个桶数据的统计个数
    int[] bucket = new int[high-low+1]; //桶
    
    //从低位到高位排序
    for(int i, j, d=1; d <= digit; d++){
        
        //置空各个桶的数据统计
        for(i=0; i < radix; i++)
            count[i] = 0;
        
        //统计各个桶的数据个数
        for(i=low; i <= high; i++){
            j = getDigit(r[i], d);
            count[j]++;
        }
        
        //累计小于桶数据 i 的数据个数
        for(i=1; i < radix; i++)
            count[i] = count[i] + count[i-1];
        
        //从右向左装桶,保证数据稳定性
        for(i=high; i >= low; i--){
            j = getDigit(r[i], d);
            bucket[count[j]-1] = r[i];
            count[j]--;
        }
        
        //倒出桶中的数据
        for(i=low, j=0; i <= high; i++, j++)
            r[i] = bucket[j];
    }
}

转载于:https://www.cnblogs.com/dwarcheng/p/5887369.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值