内部排序:希尔、快速、堆、归并等排序算法Java实现

1. 前言
排序(Sorting)是计算机程序设计中的一种重要操作,它的功能是将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序的序列。
排序方法主要分为两大类:一类是内部排序,指的是待排序记录存放在计算机随机储存器中进行的排序过程;另一类是外部排序,指的是待排序记录的数量很大,以致内存一次不能容纳全部记录,在排序过程中尚需对外存进行访问的排序过程。本篇文章讨论内部排序算法。
内部排序的方法主要有:插入排序、交换排序、选择排序和归并排序等。本篇文章针对每种排序方法给出具体的实现算法,并介绍适用场景。
1.1 测试数据
样本A:50条-200到700的随机整数;
样本B:10,000条-200到700的随机整数;
性能测试方法:使用某算法独立排序样本数据A和B各4次选择其中的三次较短的时间求平均值,平均花费时间记为TA和TB,单位毫秒(ms)。
2. 插入排序
2.1 直接插入排序(Straight Insertion Sort)
它是一种最简单的排序方法,基本操作是将一个记录插入到已排好的有序表中,从而得到一个新的、记录数增1的有序表。时间复杂度为O(n^2),空间复杂度为O(1),比较次数和移动次数约为(n^2)/4。
适用场景:记录数n不大的排序情况。
性能测试:TA=1.3ms,TB=48.3ms

/**
 * 直接插入排序:稳定的排序方法,时间复杂度为O(n^2),空间复杂度为O(1),比较次数和移动次数约为(n^2)/4
 * @author 忘川
 *
 */
public class StraightInsertionSort
{
    public static void sort(int[] data)
    {
        // 记录插入的序列
        int index = 0;
        // 记录当前要插入的值
        int value = 0;
        // 将第一个数字当作已有的序列,依次将后面的序列插入到已有的序列中
        for (int i = 1; i < data.length; i++)
        {
            // 记录当前要插入的数字
            value = data[i];
            // 从已有的序列中从后往前查找合适的位子,比较大小的同时并移动数据
            for (index = i; index > 0; index--)
            {
                if (value < data[index - 1])
                    data[index] = data[index - 1];
                else
                    break;
            }
            // 插入数据
            data[index] = value;
        }
    }
}

2.2 折半插入排序(Binary Insertion Sort)
直接插入排序,有“比较”和“移动”两个基本操作。折半插入排序采用折半查找的方式来减少比较次数,而记录的移动次数不变。折半插入排序的时间复杂度为O(n^2)
适用场景:记录数n不大的排序情况,记录数较大时性能较直接插入排序好。
性能测试:TA=1.6ms,TB=25.9ms

/**
 * 折半插入排序:时间复杂度为O(n^2)
 * 说明:折半插入排序仅减少了关键字的比较次数,而记录的移动次数不变
 * @author 忘川
 *
 */
public class BinaryInsertionSort
{
    public static void sort(int[] data)
    {
        // 记录要插入的元素
        int value = 0;
        // 用于折半查找
        int low = 0, high = 0, m = 0;
        for (int i = 1; i < data.length; i++)
        {
            // 初始化有序序列的首指针和尾指针
            low = 0;
            high = i - 1;
            // 折半查找合适的插入位置
            while (low <= high)
            {
                m = (low + high) / 2;
                // 中间值后一半查找
                if (data[i] > data[m])
                    low = m + 1;
                else
                    high = m - 1;
            }
            // 暂时存储要插入的元素
            value = data[i];
            // 向后移动元素
            for (int j = i; j > low; j--)
                data[j] = data[j - 1];
            // 插入元素
            data[low] = value;
        }
    }
}

2.3 希尔排序(Shell’s Sort)
先将整个待排序记录序列分割成若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。当增量序列为dlta[k]=2^(t-k+1)-1 (1 <= k <= t <= log2(n+1))时,时间复杂度为O(n^1.5)。
适用场景:希尔排序的时间复杂度较直接插入排序低,因此希尔排序的性能比直接插入排序优越得多。记录数n不管大小,希尔排序都有不错的表现。
性能测试:TA=1.4ms,TB=8.8ms

/**
 * 希尔排序:当增量序列为dlta[k]=2^(t-k+1)-1 (1 <= k <= t <= log2(n+1))时,O(n^1.5)
 * @author 忘川
 *
 */
public class ShellSort
{
    public static void sort(int[] data)
    {
        // shell排序的趟数:t<=log2(n+1)(向下取整)
        int t = (int) (Math.log(data.length + 1) / Math.log(2));
        // 保存Shell排序的增量序列
        int[] dlta = new int[t];
        // 计算增量序列
        for (int i = 0; i < t; i++)
            dlta[i] = (int) (Math.pow(2, t - i) - 1);
        // 进行t趟增量插入排序
        for (int i = 0; i < t; i++)
            shellInsert(data, dlta[i]);
    }

    private static void shellInsert(int[] d, int dk)
    {
        int temp = 0;
        // 把每一组第一个序列看成是已经有序的
        for (int i = dk; i < d.length; i++)
        {
            // 后面的序列比前面的小
            if (d[i] < d[i - dk])
            {
                // 暂时存储需要插入的数据
                temp = d[i];
                // 向前寻找合适的位置插入,每隔dk个序列比较一次
                int j = i - dk;
                for (; j >= 0 && d[j] > temp; j = j - dk)
                    d[j + dk] = d[j];
                // 将暂存的数据插入
                d[j + dk] = temp;
            }
        }
    }
}

3. 交换排序
3.1 冒泡排序(Bubble Sort)
若初始序列为“正序”序列,只需要进行一趟排序,在排序过程中进行n-1次关键字间的比较,且不移动记录;反之,若初始序列为“逆序”序列,则需要进行n-1趟排序,需要进行n(n-1)/2次比较,且做等数量级。因此,总的时间复杂度为O(n^2)。
适用场景:记录数n不大的排序情况。
性能测试:TA=1.1ms,TB=187.2ms

/**
 * 冒泡排序:稳定的排序算法,总的时间复杂度为O(n^2)
 * @author 忘川
 *
 */
public class BubbleSort
{
    public static void sort(int[] data)
    {
        // 记录某一趟排序是否有元素交换
        boolean flag = false;
        // 进行n-1趟排序
        for (int i = 0; i < data.length - 1; i++)
        {
            flag = false;
            for (int j = 0; j < data.length - i - 1; j++)
            {
                // 当前一个元素比后面的一个大的时候,则交换两个元素的位置
                if (data[j] > data[j + 1])
                {
                    int temp = data[j];
                    data[j] = data[j + 1];
                    data[j + 1] = temp;
                    flag = true;
                }
            }
            // 当某一趟没有交换,则说明整个序列已经有序,就终止排序
            if (!flag)
                break;
        }
    }
}

3.2 快速排序(Quick Sort)
通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序。平均时间复杂度为O(nlogn),最坏情况下的时间复杂度为O(n^2),空间复杂度为O(logn)。快速排序是目前被认为是最好的一种内部排序方法。
适用场景:性能优异,记录数n较大和较小的情况均适合。
性能测试:TA=1.2ms,TB=5.2ms

/**
 * 快速排序:不稳定的排序算法,平均时间复杂度为O(nlogn),最坏情况下的时间复杂度为O(n^2),空间复杂度为O(logn)
 * 说明:快速排序是目前被认为是最好的一种内部排序方法
 * @author 忘川
 *
 */
public class QuickSort
{
    public static void sort(int[] data)
    {
        quickSort(data, 0, data.length - 1);
    }

    private static void quickSort(int[] data, int first, int end)
    {
        if (first < end)
        {
            // 进行划分,index是轴值在数组中的下标位置
            int index = partition(data, first, end);
            // 对轴值左边的序列进行快速排序
            quickSort(data, first, index - 1);
            // 对轴值右边的序列进行快速排序
            quickSort(data, index + 1, end);
        }
    }

    /**
     * 将数列划分
     * 
     * @param data 数组
     * @param first 起始下标
     * @param end 终止下标
     * @return
     */
    private static int partition(int[] data, int first, int end)
    {
        // 记录轴值,防止数据移动
        int pivot = data[first];
        while (first < end)
        {
            // 从后往前扫描,小于轴值的数替换到前面去
            while (end > first && data[end] >= pivot)
                end--;
            data[first] = data[end];
            // 从前往后扫描,大于轴值的数替换到后面去
            while (first < end && data[first] <= pivot)
                first++;
            data[end] = data[first];
        }
        // 一次划分完成时,first和end相等
        data[first] = pivot;
        return first;
    }
}

4. 选择排序
4.1 简单选择排序(Simple Selection Sort)
将整个记录分为有序区和无序区,初始条件下有序区为空。每一趟从无序区中选出一个最小的记录和无序区的第一个记录交换,使有序区增加一个记录而无序区减少一个记录,经过n-1趟选择,整个记录已经有序。简单选择排序的时间复杂度为O(n^2)。
适用场景:记录数n不大的排序情况。
性能测试:TA=1.3ms,TB=71.3ms

/**
 * 简单选择排序:时间复杂度为O(n^2)
 * @author 忘川
 *
 */
public class SimpleSelectionSort
{
    public static void sort(int[] data)
    {
        // 记录每一趟选择中最小记录的下标
        int minIndex = 0;
        // n个记录,进行n-1趟选择
        for (int i = 0; i < data.length - 1; i++)
        {
            minIndex = i;
            // 选择余下待排序记录中的最小记录的小标
            for (int j = i + 1; j < data.length; j++)
                if (data[j] < data[minIndex])
                    minIndex = j;

            // 当前位置与余下待排序记录中的最小记录交换
            if (minIndex != i)
            {
                int temp = data[i];
                data[i] = data[minIndex];
                data[minIndex] = temp;
            }
        }
    }
}

4.2 堆排序(Heap Sort)
堆排序运行时间主要耗费在建初始堆和调整建新堆时进行的反复“筛选”上,堆排序在最坏的情况下,其时间复杂度为O(nlogn),相对于快速排序来说,这是堆排序最大的优点。堆排序的空间复杂度为O(1)。
适用场景:堆排序方法对记录数较少的文件并不值得提倡,但对n较大的文件还是很有效的。
性能测试:TA=1.3ms,TB=4.5ms

/**
 * 堆排序:时间复杂度为O(log2(n))
 * 说明:堆排序对n值较大的序列排序很有效。空间复杂度为O(1),最坏情况时间复杂度为O(nlogn),
 * 这是相当于快速排序最大的优点
 * @author 忘川
 *
 */
public class HeapSort
{
    public static void sort(int[] data)
    {
        // 将一个乱序序列调整成堆,参与调整的序列为数组的长度
        for (int i = (data.length - 1) / 2; i >= 0; i--)
            siftHeap(data, i, data.length);

        // 进行n-1趟选择
        for (int i = 1; i < data.length; i++)
        {
            // 将二叉树的root和乱序区的最后一个节点交换
            int temp = data[0];
            data[0] = data[data.length - i];
            data[data.length - i] = temp;
            // 重新将乱序区的序列调整成堆
            siftHeap(data, 0, data.length - i);
        }
    }

    /**
     * 将乱序序列调整成堆
     * @param data 乱序序列
     * @param k 要调整的元素的下标
     * @param count 乱序区元素的长度
     */
    private static void siftHeap(int[] data, int k, int count)
    {
        // 初始值为做孩子的下标
        int max = 2 * (k + 1) - 1;
        // 当该节点不是叶子节点时(已经排序好的节点认为已经从二叉树上减掉了),进行筛选
        // 当右孩子的序号小于等于乱序区元素的个数时,就可以进行筛选
        while (max + 1 <= count)
        {
            // 选取左右孩子较大元素的下标值,当没有右孩子时,左孩子就是最大的
            if (max < count - 1 && data[max] < data[max + 1])
                max++;

            // 如果当前节点大于左右孩子节点,则筛选完毕。因为其左右孩子节点本身就是堆,
            // 而其兄弟本身就是堆并且并没有参与交换依然是堆
            if (data[k] >= data[max])
                break;
            else
            {
                // 将该节点与左右孩子中较大孩子交换
                int temp = data[k];
                data[k] = data[max];
                data[max] = temp;
                // 筛选目标变为左右孩子中参与交换的那个孩子,另一个孩子本身是堆不用继续筛选
                k = max;
                max = 2 * (k + 1) - 1;
            }
        }
    }
}

5. 归并排序(Merging Sort)
“归并”的含义就是将两个或两个以上的有序表组合成一个新的有序表。实现归并排序需要和待排序记录等数量的辅助空间,其空间复杂度为O(n)。归并排序递归形式的算法的形式上较为简洁,但使用性很差(例子给出了递归实现和非递归实现)。归并排序与快速排序和堆排序相比,它最大的特点就是:归并排序是一种稳定的排序方法。归并排序的时间复杂度为O(nlogn)。
适用场景:由于空间复杂度为O(n),不适合记录数n值较大的情况。
性能测试:TA=1.3ms,TB=6.2ms

/**
 * 归并排序:时间复杂度为O(nlogn),空间复杂度为O(n),它需要等长的序列存放归并之后的序列
 * @author 忘川
 *
 */
public class MergingSort
{

    /**
     * 用归并排序算法对序列进行排序
     * @param data 待排序序列
     * @param flag 0采用递归算法,非0则采用非递归算法
     */
    public static void sort(int[] data, int flag)
    {
        if (flag == 0)
            sortByRecursion(data, new int[data.length], 0, data.length - 1);
        else
            sortNotRecursion(data);
    }

    /**
     * 归并排序算法的非递归实现
     * @param data 待排序的序列
     */
    private static void sortNotRecursion(int[] data)
    {
        //一趟归并排序扫描每次处理的数据的个数
        //例如h=2,则表示将两个长度为1的序列归并为一个长度为2的序列
        int h = 2;
        //辅助数组,用于两个序列合并为一个序列用
        int[] t = new int[data.length];
        //当每次处理的数据大于等于待排序数组的长度时,说明已经排序完成
        for (int c = 1; (h = (int) Math.pow(2, c)) < data.length; c++)
        {
            int i = 0;
            //将前n*h个分组的数据分别处理(每h个数据一分为二两两合并),n为整数,最后尾部可能余下不足h个数据
            for (i = 0; i + h <= data.length; i = i + h)
                merge(data, t, i, i + h / 2 - 1, i + h - 1);
            //将尾部余下的不足h个数据与第n个分组(有h个)的合并
            if (i != data.length)
                merge(data, t, i - h, i - 1, data.length - 1);
        }
    }

    /**
     * 归并排序算法的递归实现
     * @param d 待排序序列
     * @param t 辅助数组
     * @param start 待排序序列的起始下标
     * @param end 待排序序列的结束下标
     */
    private static void sortByRecursion(int[] d, int[] t, int start, int end)
    {
        // 当只有一个记录时,已经有序
        if (start == end)
            return;
        else
        {
            int mid = (start + end) / 2;
            // 对前一半进行递归归并排序,排序结果通过辅助数组存放在d[start]……d[mid]中
            sortByRecursion(d, t, start, mid);
            // 对后一般进行递归归并排序,排序结果存放在d[mid+1]……d[end]中
            sortByRecursion(d, t, mid + 1, end);
            // 将d[start]……d[mid]和d[mid+1]……d[end]两部分有序的序列合并,结果存放在t[start]……t[end]中
            merge(d, t, start, mid, end);
        }
    }

    /**
     * 将d[start]……d[mid]和d[mid+1]……d[end]两部分有序的序列合并,
     * 并借助辅助数组放数据将合并之后的数据存放到d[start]……d[end]中
     * @param d 序列
     * @param t 辅助数组
     * @param start 第一部分有序序列开始下标
     * @param mid 第一部分有序序列结束下标
     * @param end 第二部分有序序列结束下标
     */
    private static void merge(int[] d, int[] t, int start, int mid, int end)
    {
        // 将d[start]……d[mid]和d[mid+1]……d[end]两部分有序的序列合并,结果存放在t[start]……t[end]中
        int index1 = start, index2 = mid + 1;
        int tIndex = start;
        while (index1 <= mid && index2 <= end)
        {
            // 如果前者小,则t中放入前者,前者指示器增加一
            if (d[index1] < d[index2])
                t[tIndex++] = d[index1++];
            else
                t[tIndex++] = d[index2++];
        }
        // 将其中一个没有处理完的序列进行收尾处理
        if (index1 > mid)
            while (index2 <= end)
                t[tIndex++] = d[index2++];
        if (index2 > end)
            while (index1 <= mid)
                t[tIndex++] = d[index1++];

        // 将辅助数组中暂时存放的数据传回原数组,达到两个序列合并为一个序列的目的
        for (int i = start; i <= end; i++)
            d[i] = t[i];
    }
}

6. 总结
(1)从平均时间性能而言,快速排序最佳,其所需时间最省,但快速排序在最坏情况下的时间性能不如堆排序和归并排序。而后两者相比较的结果是,在n较大时,归并排序所需的时间较堆排序省,但它所需的辅助存储量最多(性能测试中堆排序性能非常好的原因和n值不够大以及样本数据有较多相等有关)。
(2)简单的排序方法包括除希尔排序之外的所有插入排序、冒泡排序和简单选择排序,其中直接插入排序最简单,当序列中的记录“基本有序”或n值较小时,它是最佳的排序方法。
(3)从方法的稳定性来比较,所有的时间复杂度为O(n^2)的简单排序方法是稳定的,而快速排序、堆排序和希尔排序等时间性能较好的排序方法都是不稳定的。
综上所述,本节讨论的所有排序方法中,没有哪一种是绝对最优的。有的适合于n较大的情况,有的适合于n较小的情况,因此,在实用时需根据不同情况适当选择,甚至可将多种方法结合起来使用。

7. 附录
算法及测试代码:http://download.csdn.net/detail/mytroy/8572277

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值